From d281d765ea87642b270d418cccb9fea408b66d5e Mon Sep 17 00:00:00 2001 From: Giuseppe Nucifora Date: Wed, 24 Feb 2016 15:45:24 +0100 Subject: [PATCH] First Commit --- .../PNXMPPFramework.xcodeproj/project.pbxproj | 135 +- .../contents.xcworkspacedata | 10 + Example/Podfile | 4 +- Example/Podfile.lock | 49 + Example/Pods/CocoaAsyncSocket/README.markdown | 121 + .../Source/GCD/GCDAsyncSocket.h | 1197 +++ .../Source/GCD/GCDAsyncSocket.m | 8239 +++++++++++++++++ .../Source/GCD/GCDAsyncUdpSocket.h | 1007 ++ .../Source/GCD/GCDAsyncUdpSocket.m | 5430 +++++++++++ .../Source/RunLoop/AsyncSocket.h | 657 ++ .../Source/RunLoop/AsyncSocket.m | 4313 +++++++++ .../Source/RunLoop/AsyncUdpSocket.h | 368 + .../Source/RunLoop/AsyncUdpSocket.m | 2313 +++++ .../CocoaLumberjack/Classes/CocoaLumberjack.h | 81 + .../Classes/CocoaLumberjack.swift | 91 + .../CocoaLumberjack/Classes/DDASLLogCapture.h | 48 + .../CocoaLumberjack/Classes/DDASLLogCapture.m | 230 + .../CocoaLumberjack/Classes/DDASLLogger.h | 58 + .../CocoaLumberjack/Classes/DDASLLogger.m | 121 + .../Classes/DDAbstractDatabaseLogger.h | 123 + .../Classes/DDAbstractDatabaseLogger.m | 660 ++ .../CocoaLumberjack/Classes/DDAssertMacros.h | 26 + .../CocoaLumberjack/Classes/DDFileLogger.h | 487 + .../CocoaLumberjack/Classes/DDFileLogger.m | 1484 +++ .../CocoaLumberjack/Classes/DDLegacyMacros.h | 75 + .../Pods/CocoaLumberjack/Classes/DDLog+LOGV.h | 83 + Example/Pods/CocoaLumberjack/Classes/DDLog.h | 743 ++ Example/Pods/CocoaLumberjack/Classes/DDLog.m | 1167 +++ .../CocoaLumberjack/Classes/DDLogMacros.h | 82 + .../CocoaLumberjack/Classes/DDTTYLogger.h | 178 + .../CocoaLumberjack/Classes/DDTTYLogger.m | 1481 +++ .../Extensions/DDContextFilterLogFormatter.h | 117 + .../Extensions/DDContextFilterLogFormatter.m | 191 + .../Extensions/DDDispatchQueueLogFormatter.h | 178 + .../Extensions/DDDispatchQueueLogFormatter.m | 277 + .../Classes/Extensions/DDMultiFormatter.h | 56 + .../Classes/Extensions/DDMultiFormatter.m | 144 + .../Lumberjack/CocoaLumberjack.modulemap | 36 + Example/Pods/CocoaLumberjack/LICENSE.txt | 18 + Example/Pods/CocoaLumberjack/README.md | 197 + .../FBSnapshotTestCase.modulemap | 15 + .../Categories/UIImage+Compare.h | 37 + .../Categories/UIImage+Compare.m | 134 + .../Categories/UIImage+Diff.h | 37 + .../Categories/UIImage+Diff.m | 56 + .../Categories/UIImage+Snapshot.h | 24 + .../Categories/UIImage+Snapshot.m | 62 + .../FBSnapshotTestCase/FBSnapshotTestCase.h | 200 + .../FBSnapshotTestCase/FBSnapshotTestCase.m | 136 + .../FBSnapshotTestCasePlatform.h | 44 + .../FBSnapshotTestCasePlatform.m | 49 + .../FBSnapshotTestController.h | 151 + .../FBSnapshotTestController.m | 356 + .../FBSnapshotTestCase/SwiftSupport.swift | 66 + Example/Pods/FBSnapshotTestCase/LICENSE | 29 + Example/Pods/FBSnapshotTestCase/README.md | 97 + .../KissXML/Additions/DDXMLElementAdditions.h | 24 + .../KissXML/Additions/DDXMLElementAdditions.m | 131 + .../KissXML/Categories/NSString+DDXML.h | 24 + .../KissXML/Categories/NSString+DDXML.m | 31 + Example/Pods/KissXML/KissXML/DDXML.h | 194 + Example/Pods/KissXML/KissXML/DDXMLDocument.h | 84 + Example/Pods/KissXML/KissXML/DDXMLDocument.m | 140 + Example/Pods/KissXML/KissXML/DDXMLElement.h | 66 + Example/Pods/KissXML/KissXML/DDXMLElement.m | 801 ++ Example/Pods/KissXML/KissXML/DDXMLNode.h | 167 + Example/Pods/KissXML/KissXML/DDXMLNode.m | 2905 ++++++ .../KissXML/KissXML/Private/DDXMLPrivate.h | 229 + Example/Pods/KissXML/LICENSE.txt | 8 + Example/Pods/KissXML/README.markdown | 34 + .../PNXMPPFramework.podspec.json | 39 + Example/Pods/Manifest.lock | 49 + Example/Pods/Pods.xcodeproj/project.pbxproj | 1842 ++++ .../xcschemes/PNXMPPFramework.xcscheme | 60 + .../CocoaAsyncSocket/CocoaAsyncSocket-dummy.m | 5 + .../CocoaAsyncSocket-prefix.pch | 4 + .../CocoaAsyncSocket-umbrella.h | 10 + .../CocoaAsyncSocket.modulemap | 6 + .../CocoaAsyncSocket.xcconfig | 6 + .../CocoaAsyncSocket/Info.plist | 26 + .../CocoaLumberjack/CocoaLumberjack-dummy.m | 5 + .../CocoaLumberjack-prefix.pch | 4 + .../CocoaLumberjack/CocoaLumberjack.modulemap | 36 + .../CocoaLumberjack/CocoaLumberjack.xcconfig | 5 + .../CocoaLumberjack/Info.plist | 26 + .../FBSnapshotTestCase-dummy.m | 5 + .../FBSnapshotTestCase-prefix.pch | 4 + .../FBSnapshotTestCase.modulemap | 15 + .../FBSnapshotTestCase.xcconfig | 9 + .../FBSnapshotTestCase/Info.plist | 26 + .../Target Support Files/KissXML/Info.plist | 26 + .../KissXML/KissXML-dummy.m | 5 + .../KissXML/KissXML-prefix.pch | 4 + .../KissXML/KissXML-umbrella.h | 13 + .../KissXML/KissXML.modulemap | 6 + .../KissXML/KissXML.xcconfig | 8 + .../PNXMPPFramework/Info.plist | 26 + .../PNXMPPFramework/PNXMPPFramework-dummy.m | 5 + .../PNXMPPFramework-prefix.pch | 4 + .../PNXMPPFramework-umbrella.h | 6 + .../PNXMPPFramework/PNXMPPFramework.modulemap | 6 + .../PNXMPPFramework/PNXMPPFramework.xcconfig | 5 + .../ResourceBundle-PNXMPPFramework-Info.plist | 24 + .../Pods-PNXMPPFramework_Example/Info.plist | 26 + ...ramework_Example-acknowledgements.markdown | 68 + ...PPFramework_Example-acknowledgements.plist | 110 + .../Pods-PNXMPPFramework_Example-dummy.m | 5 + ...Pods-PNXMPPFramework_Example-frameworks.sh | 97 + .../Pods-PNXMPPFramework_Example-resources.sh | 102 + .../Pods-PNXMPPFramework_Example-umbrella.h | 6 + ...ods-PNXMPPFramework_Example.debug.xcconfig | 8 + .../Pods-PNXMPPFramework_Example.modulemap | 6 + ...s-PNXMPPFramework_Example.release.xcconfig | 8 + .../Pods-PNXMPPFramework_Tests/Info.plist | 26 + ...PFramework_Tests-acknowledgements.markdown | 101 + ...XMPPFramework_Tests-acknowledgements.plist | 147 + .../Pods-PNXMPPFramework_Tests-dummy.m | 5 + .../Pods-PNXMPPFramework_Tests-frameworks.sh | 99 + .../Pods-PNXMPPFramework_Tests-resources.sh | 102 + .../Pods-PNXMPPFramework_Tests-umbrella.h | 6 + .../Pods-PNXMPPFramework_Tests.debug.xcconfig | 10 + .../Pods-PNXMPPFramework_Tests.modulemap | 6 + ...ods-PNXMPPFramework_Tests.release.xcconfig | 10 + PNXMPPFramework.podspec | 8 +- 124 files changed, 41873 insertions(+), 9 deletions(-) create mode 100644 Example/PNXMPPFramework.xcworkspace/contents.xcworkspacedata create mode 100644 Example/Podfile.lock create mode 100644 Example/Pods/CocoaAsyncSocket/README.markdown create mode 100644 Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.h create mode 100644 Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m create mode 100644 Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncUdpSocket.h create mode 100644 Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncUdpSocket.m create mode 100644 Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncSocket.h create mode 100644 Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncSocket.m create mode 100644 Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncUdpSocket.h create mode 100644 Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncUdpSocket.m create mode 100644 Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.h create mode 100644 Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.swift create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.h create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.m create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDASLLogger.h create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDASLLogger.m create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.h create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.m create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDAssertMacros.h create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDFileLogger.h create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDFileLogger.m create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDLegacyMacros.h create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDLog+LOGV.h create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDLog.h create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDLog.m create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDLogMacros.h create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDTTYLogger.h create mode 100644 Example/Pods/CocoaLumberjack/Classes/DDTTYLogger.m create mode 100644 Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.h create mode 100644 Example/Pods/CocoaLumberjack/Classes/Extensions/DDContextFilterLogFormatter.m create mode 100644 Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.h create mode 100644 Example/Pods/CocoaLumberjack/Classes/Extensions/DDDispatchQueueLogFormatter.m create mode 100644 Example/Pods/CocoaLumberjack/Classes/Extensions/DDMultiFormatter.h create mode 100644 Example/Pods/CocoaLumberjack/Classes/Extensions/DDMultiFormatter.m create mode 100644 Example/Pods/CocoaLumberjack/Framework/Lumberjack/CocoaLumberjack.modulemap create mode 100644 Example/Pods/CocoaLumberjack/LICENSE.txt create mode 100644 Example/Pods/CocoaLumberjack/README.md create mode 100644 Example/Pods/FBSnapshotTestCase/FBSnapshotTestCase.modulemap create mode 100644 Example/Pods/FBSnapshotTestCase/FBSnapshotTestCase/Categories/UIImage+Compare.h create mode 100644 Example/Pods/FBSnapshotTestCase/FBSnapshotTestCase/Categories/UIImage+Compare.m create mode 100644 Example/Pods/FBSnapshotTestCase/FBSnapshotTestCase/Categories/UIImage+Diff.h create mode 100644 Example/Pods/FBSnapshotTestCase/FBSnapshotTestCase/Categories/UIImage+Diff.m create mode 100644 Example/Pods/FBSnapshotTestCase/FBSnapshotTestCase/Categories/UIImage+Snapshot.h create mode 100644 Example/Pods/FBSnapshotTestCase/FBSnapshotTestCase/Categories/UIImage+Snapshot.m create mode 100644 Example/Pods/FBSnapshotTestCase/FBSnapshotTestCase/FBSnapshotTestCase.h create mode 100644 Example/Pods/FBSnapshotTestCase/FBSnapshotTestCase/FBSnapshotTestCase.m create mode 100644 Example/Pods/FBSnapshotTestCase/FBSnapshotTestCase/FBSnapshotTestCasePlatform.h create mode 100644 Example/Pods/FBSnapshotTestCase/FBSnapshotTestCase/FBSnapshotTestCasePlatform.m create mode 100644 Example/Pods/FBSnapshotTestCase/FBSnapshotTestCase/FBSnapshotTestController.h create mode 100644 Example/Pods/FBSnapshotTestCase/FBSnapshotTestCase/FBSnapshotTestController.m create mode 100644 Example/Pods/FBSnapshotTestCase/FBSnapshotTestCase/SwiftSupport.swift create mode 100644 Example/Pods/FBSnapshotTestCase/LICENSE create mode 100644 Example/Pods/FBSnapshotTestCase/README.md create mode 100644 Example/Pods/KissXML/KissXML/Additions/DDXMLElementAdditions.h create mode 100644 Example/Pods/KissXML/KissXML/Additions/DDXMLElementAdditions.m create mode 100644 Example/Pods/KissXML/KissXML/Categories/NSString+DDXML.h create mode 100644 Example/Pods/KissXML/KissXML/Categories/NSString+DDXML.m create mode 100644 Example/Pods/KissXML/KissXML/DDXML.h create mode 100644 Example/Pods/KissXML/KissXML/DDXMLDocument.h create mode 100644 Example/Pods/KissXML/KissXML/DDXMLDocument.m create mode 100644 Example/Pods/KissXML/KissXML/DDXMLElement.h create mode 100644 Example/Pods/KissXML/KissXML/DDXMLElement.m create mode 100644 Example/Pods/KissXML/KissXML/DDXMLNode.h create mode 100644 Example/Pods/KissXML/KissXML/DDXMLNode.m create mode 100644 Example/Pods/KissXML/KissXML/Private/DDXMLPrivate.h create mode 100644 Example/Pods/KissXML/LICENSE.txt create mode 100644 Example/Pods/KissXML/README.markdown create mode 100644 Example/Pods/Local Podspecs/PNXMPPFramework.podspec.json create mode 100644 Example/Pods/Manifest.lock create mode 100644 Example/Pods/Pods.xcodeproj/project.pbxproj create mode 100644 Example/Pods/Pods.xcodeproj/xcshareddata/xcschemes/PNXMPPFramework.xcscheme create mode 100644 Example/Pods/Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket-dummy.m create mode 100644 Example/Pods/Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket-prefix.pch create mode 100644 Example/Pods/Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket-umbrella.h create mode 100644 Example/Pods/Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket.modulemap create mode 100644 Example/Pods/Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket.xcconfig create mode 100644 Example/Pods/Target Support Files/CocoaAsyncSocket/Info.plist create mode 100644 Example/Pods/Target Support Files/CocoaLumberjack/CocoaLumberjack-dummy.m create mode 100644 Example/Pods/Target Support Files/CocoaLumberjack/CocoaLumberjack-prefix.pch create mode 100644 Example/Pods/Target Support Files/CocoaLumberjack/CocoaLumberjack.modulemap create mode 100644 Example/Pods/Target Support Files/CocoaLumberjack/CocoaLumberjack.xcconfig create mode 100644 Example/Pods/Target Support Files/CocoaLumberjack/Info.plist create mode 100644 Example/Pods/Target Support Files/FBSnapshotTestCase/FBSnapshotTestCase-dummy.m create mode 100644 Example/Pods/Target Support Files/FBSnapshotTestCase/FBSnapshotTestCase-prefix.pch create mode 100644 Example/Pods/Target Support Files/FBSnapshotTestCase/FBSnapshotTestCase.modulemap create mode 100644 Example/Pods/Target Support Files/FBSnapshotTestCase/FBSnapshotTestCase.xcconfig create mode 100644 Example/Pods/Target Support Files/FBSnapshotTestCase/Info.plist create mode 100644 Example/Pods/Target Support Files/KissXML/Info.plist create mode 100644 Example/Pods/Target Support Files/KissXML/KissXML-dummy.m create mode 100644 Example/Pods/Target Support Files/KissXML/KissXML-prefix.pch create mode 100644 Example/Pods/Target Support Files/KissXML/KissXML-umbrella.h create mode 100644 Example/Pods/Target Support Files/KissXML/KissXML.modulemap create mode 100644 Example/Pods/Target Support Files/KissXML/KissXML.xcconfig create mode 100644 Example/Pods/Target Support Files/PNXMPPFramework/Info.plist create mode 100644 Example/Pods/Target Support Files/PNXMPPFramework/PNXMPPFramework-dummy.m create mode 100644 Example/Pods/Target Support Files/PNXMPPFramework/PNXMPPFramework-prefix.pch create mode 100644 Example/Pods/Target Support Files/PNXMPPFramework/PNXMPPFramework-umbrella.h create mode 100644 Example/Pods/Target Support Files/PNXMPPFramework/PNXMPPFramework.modulemap create mode 100644 Example/Pods/Target Support Files/PNXMPPFramework/PNXMPPFramework.xcconfig create mode 100644 Example/Pods/Target Support Files/PNXMPPFramework/ResourceBundle-PNXMPPFramework-Info.plist create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Example/Info.plist create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Example/Pods-PNXMPPFramework_Example-acknowledgements.markdown create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Example/Pods-PNXMPPFramework_Example-acknowledgements.plist create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Example/Pods-PNXMPPFramework_Example-dummy.m create mode 100755 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Example/Pods-PNXMPPFramework_Example-frameworks.sh create mode 100755 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Example/Pods-PNXMPPFramework_Example-resources.sh create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Example/Pods-PNXMPPFramework_Example-umbrella.h create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Example/Pods-PNXMPPFramework_Example.debug.xcconfig create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Example/Pods-PNXMPPFramework_Example.modulemap create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Example/Pods-PNXMPPFramework_Example.release.xcconfig create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Tests/Info.plist create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Tests/Pods-PNXMPPFramework_Tests-acknowledgements.markdown create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Tests/Pods-PNXMPPFramework_Tests-acknowledgements.plist create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Tests/Pods-PNXMPPFramework_Tests-dummy.m create mode 100755 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Tests/Pods-PNXMPPFramework_Tests-frameworks.sh create mode 100755 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Tests/Pods-PNXMPPFramework_Tests-resources.sh create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Tests/Pods-PNXMPPFramework_Tests-umbrella.h create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Tests/Pods-PNXMPPFramework_Tests.debug.xcconfig create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Tests/Pods-PNXMPPFramework_Tests.modulemap create mode 100644 Example/Pods/Target Support Files/Pods-PNXMPPFramework_Tests/Pods-PNXMPPFramework_Tests.release.xcconfig diff --git a/Example/PNXMPPFramework.xcodeproj/project.pbxproj b/Example/PNXMPPFramework.xcodeproj/project.pbxproj index 2166b0a..32f662e 100644 --- a/Example/PNXMPPFramework.xcodeproj/project.pbxproj +++ b/Example/PNXMPPFramework.xcodeproj/project.pbxproj @@ -20,7 +20,9 @@ 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F591195388D20070C39A /* UIKit.framework */; }; 6003F5BA195388D20070C39A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; 6003F5BC195388D20070C39A /* Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6003F5BB195388D20070C39A /* Tests.m */; }; + 749B868208C4550DF8D15244 /* Pods_PNXMPPFramework_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0BC15E8E255445D1654538AD /* Pods_PNXMPPFramework_Example.framework */; }; 873B8AEB1B1F5CCA007FD442 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */; }; + ED4A1BF717CEBC9857A3C141 /* Pods_PNXMPPFramework_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F3BF4B442CB9D5C33829E65 /* Pods_PNXMPPFramework_Tests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -34,7 +36,11 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 48A2780CBF4129B217C9FB60 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file; name = README.md; path = ../README.md; sourceTree = ""; }; + 07D73116CC9292248C2B77E0 /* Pods-PNXMPPFramework_Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PNXMPPFramework_Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PNXMPPFramework_Tests/Pods-PNXMPPFramework_Tests.debug.xcconfig"; sourceTree = ""; }; + 0BC15E8E255445D1654538AD /* Pods_PNXMPPFramework_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PNXMPPFramework_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 48A2780CBF4129B217C9FB60 /* README.md */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = net.daringfireball.markdown; name = README.md; path = ../README.md; sourceTree = ""; }; + 55F146B95BAE799C9D044DE9 /* Pods-PNXMPPFramework_Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PNXMPPFramework_Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-PNXMPPFramework_Tests/Pods-PNXMPPFramework_Tests.release.xcconfig"; sourceTree = ""; }; + 5F3BF4B442CB9D5C33829E65 /* Pods_PNXMPPFramework_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PNXMPPFramework_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6003F58A195388D20070C39A /* PNXMPPFramework_Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PNXMPPFramework_Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 6003F58D195388D20070C39A /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 6003F58F195388D20070C39A /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; @@ -55,7 +61,9 @@ 6003F5BB195388D20070C39A /* Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Tests.m; sourceTree = ""; }; 606FC2411953D9B200FFA9A0 /* Tests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tests-Prefix.pch"; sourceTree = ""; }; 873B8AEA1B1F5CCA007FD442 /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; }; - 9C9E2AE32D26AF684C48C9EF /* PNXMPPFramework.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file; name = PNXMPPFramework.podspec; path = ../PNXMPPFramework.podspec; sourceTree = ""; }; + 9C6A5AC04DA60C754426EF31 /* Pods-PNXMPPFramework_Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PNXMPPFramework_Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-PNXMPPFramework_Example/Pods-PNXMPPFramework_Example.debug.xcconfig"; sourceTree = ""; }; + 9C9E2AE32D26AF684C48C9EF /* PNXMPPFramework.podspec */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = PNXMPPFramework.podspec; path = ../PNXMPPFramework.podspec; sourceTree = ""; }; + C7E6916AC71C9FA4641759C4 /* Pods-PNXMPPFramework_Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PNXMPPFramework_Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-PNXMPPFramework_Example/Pods-PNXMPPFramework_Example.release.xcconfig"; sourceTree = ""; }; FC794850FC701B180023C6D3 /* LICENSE */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; name = LICENSE; path = ../LICENSE; sourceTree = ""; }; /* End PBXFileReference section */ @@ -67,6 +75,7 @@ 6003F590195388D20070C39A /* CoreGraphics.framework in Frameworks */, 6003F592195388D20070C39A /* UIKit.framework in Frameworks */, 6003F58E195388D20070C39A /* Foundation.framework in Frameworks */, + 749B868208C4550DF8D15244 /* Pods_PNXMPPFramework_Example.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -77,6 +86,7 @@ 6003F5B0195388D20070C39A /* XCTest.framework in Frameworks */, 6003F5B2195388D20070C39A /* UIKit.framework in Frameworks */, 6003F5B1195388D20070C39A /* Foundation.framework in Frameworks */, + ED4A1BF717CEBC9857A3C141 /* Pods_PNXMPPFramework_Tests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -91,6 +101,7 @@ 6003F5B5195388D20070C39A /* Tests */, 6003F58C195388D20070C39A /* Frameworks */, 6003F58B195388D20070C39A /* Products */, + FA2286D5A97A795EA8C4D32A /* Pods */, ); sourceTree = ""; }; @@ -110,6 +121,8 @@ 6003F58F195388D20070C39A /* CoreGraphics.framework */, 6003F591195388D20070C39A /* UIKit.framework */, 6003F5AF195388D20070C39A /* XCTest.framework */, + 0BC15E8E255445D1654538AD /* Pods_PNXMPPFramework_Example.framework */, + 5F3BF4B442CB9D5C33829E65 /* Pods_PNXMPPFramework_Tests.framework */, ); name = Frameworks; sourceTree = ""; @@ -169,6 +182,17 @@ name = "Podspec Metadata"; sourceTree = ""; }; + FA2286D5A97A795EA8C4D32A /* Pods */ = { + isa = PBXGroup; + children = ( + 9C6A5AC04DA60C754426EF31 /* Pods-PNXMPPFramework_Example.debug.xcconfig */, + C7E6916AC71C9FA4641759C4 /* Pods-PNXMPPFramework_Example.release.xcconfig */, + 07D73116CC9292248C2B77E0 /* Pods-PNXMPPFramework_Tests.debug.xcconfig */, + 55F146B95BAE799C9D044DE9 /* Pods-PNXMPPFramework_Tests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -176,9 +200,12 @@ isa = PBXNativeTarget; buildConfigurationList = 6003F5BF195388D20070C39A /* Build configuration list for PBXNativeTarget "PNXMPPFramework_Example" */; buildPhases = ( + 9176D54A4AF0F229C88A9A6E /* Check Pods Manifest.lock */, 6003F586195388D20070C39A /* Sources */, 6003F587195388D20070C39A /* Frameworks */, 6003F588195388D20070C39A /* Resources */, + BAF822DE65D2F60DDDDA6DB7 /* Embed Pods Frameworks */, + 5B9320A0153767C5710AC930 /* Copy Pods Resources */, ); buildRules = ( ); @@ -193,9 +220,12 @@ isa = PBXNativeTarget; buildConfigurationList = 6003F5C2195388D20070C39A /* Build configuration list for PBXNativeTarget "PNXMPPFramework_Tests" */; buildPhases = ( + DC6BEFF10A902FC868C2D943 /* Check Pods Manifest.lock */, 6003F5AA195388D20070C39A /* Sources */, 6003F5AB195388D20070C39A /* Frameworks */, 6003F5AC195388D20070C39A /* Resources */, + B5E49261AF5038095695C7E6 /* Embed Pods Frameworks */, + 8CD7E296CF475455022D0388 /* Copy Pods Resources */, ); buildRules = ( ); @@ -222,7 +252,7 @@ }; }; }; - buildConfigurationList = 6003F585195388D10070C39A /* Build configuration list for PBXProject "PROJECT" */; + buildConfigurationList = 6003F585195388D10070C39A /* Build configuration list for PBXProject "PNXMPPFramework" */; compatibilityVersion = "Xcode 3.2"; developmentRegion = English; hasScannedForEncodings = 0; @@ -262,6 +292,99 @@ }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 5B9320A0153767C5710AC930 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PNXMPPFramework_Example/Pods-PNXMPPFramework_Example-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8CD7E296CF475455022D0388 /* Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Pods Resources"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PNXMPPFramework_Tests/Pods-PNXMPPFramework_Tests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9176D54A4AF0F229C88A9A6E /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; + B5E49261AF5038095695C7E6 /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PNXMPPFramework_Tests/Pods-PNXMPPFramework_Tests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + BAF822DE65D2F60DDDDA6DB7 /* Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Embed Pods Frameworks"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-PNXMPPFramework_Example/Pods-PNXMPPFramework_Example-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + DC6BEFF10A902FC868C2D943 /* Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Check Pods Manifest.lock"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 6003F586195388D20070C39A /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -386,6 +509,7 @@ }; 6003F5C0195388D20070C39A /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9C6A5AC04DA60C754426EF31 /* Pods-PNXMPPFramework_Example.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; @@ -401,6 +525,7 @@ }; 6003F5C1195388D20070C39A /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = C7E6916AC71C9FA4641759C4 /* Pods-PNXMPPFramework_Example.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; @@ -416,6 +541,7 @@ }; 6003F5C3195388D20070C39A /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 07D73116CC9292248C2B77E0 /* Pods-PNXMPPFramework_Tests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; FRAMEWORK_SEARCH_PATHS = ( @@ -439,6 +565,7 @@ }; 6003F5C4195388D20070C39A /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 55F146B95BAE799C9D044DE9 /* Pods-PNXMPPFramework_Tests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; FRAMEWORK_SEARCH_PATHS = ( @@ -459,7 +586,7 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 6003F585195388D10070C39A /* Build configuration list for PBXProject "PROJECT" */ = { + 6003F585195388D10070C39A /* Build configuration list for PBXProject "PNXMPPFramework" */ = { isa = XCConfigurationList; buildConfigurations = ( 6003F5BD195388D20070C39A /* Debug */, diff --git a/Example/PNXMPPFramework.xcworkspace/contents.xcworkspacedata b/Example/PNXMPPFramework.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..bf27494 --- /dev/null +++ b/Example/PNXMPPFramework.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Example/Podfile b/Example/Podfile index ee7c5a8..25261eb 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -1,11 +1,11 @@ source 'https://github.com/CocoaPods/Specs.git' use_frameworks! -target 'PNXMPPFramework_Example', :exclusive => true do +target 'PNXMPPFramework_Example' do pod 'PNXMPPFramework', :path => '../' end -target 'PNXMPPFramework_Tests', :exclusive => true do +target 'PNXMPPFramework_Tests' do pod 'PNXMPPFramework', :path => '../' pod 'FBSnapshotTestCase' diff --git a/Example/Podfile.lock b/Example/Podfile.lock new file mode 100644 index 0000000..e9e0ea4 --- /dev/null +++ b/Example/Podfile.lock @@ -0,0 +1,49 @@ +PODS: + - CocoaAsyncSocket (7.4.3): + - CocoaAsyncSocket/All (= 7.4.3) + - CocoaAsyncSocket/All (7.4.3): + - CocoaAsyncSocket/GCD + - CocoaAsyncSocket/RunLoop + - CocoaAsyncSocket/GCD (7.4.3) + - CocoaAsyncSocket/RunLoop (7.4.3) + - CocoaLumberjack (2.2.0): + - CocoaLumberjack/Default (= 2.2.0) + - CocoaLumberjack/Extensions (= 2.2.0) + - CocoaLumberjack/Core (2.2.0) + - CocoaLumberjack/Default (2.2.0): + - CocoaLumberjack/Core + - CocoaLumberjack/Extensions (2.2.0): + - CocoaLumberjack/Default + - FBSnapshotTestCase (2.0.7): + - FBSnapshotTestCase/SwiftSupport (= 2.0.7) + - FBSnapshotTestCase/Core (2.0.7) + - FBSnapshotTestCase/SwiftSupport (2.0.7): + - FBSnapshotTestCase/Core + - KissXML (5.0.3): + - KissXML/Standard (= 5.0.3) + - KissXML/Core (5.0.3) + - KissXML/Standard (5.0.3): + - KissXML/Core + - PNXMPPFramework (0.1.0): + - CocoaAsyncSocket + - CocoaLumberjack + - KissXML + +DEPENDENCIES: + - FBSnapshotTestCase + - PNXMPPFramework (from `../`) + +EXTERNAL SOURCES: + PNXMPPFramework: + :path: "../" + +SPEC CHECKSUMS: + CocoaAsyncSocket: a18c75dca4b08723628a0bacca6e94803d90be91 + CocoaLumberjack: 17fe8581f84914d5d7e6360f7c70022b173c3ae0 + FBSnapshotTestCase: 7e85180d0d141a0cf472352edda7e80d7eaeb547 + KissXML: d19dd6dc65e0dc721ba92b3077b8ebdd240f1c1e + PNXMPPFramework: 85a177de196fd742392f6ed0053c9cd2dd160f06 + +PODFILE CHECKSUM: c24dacdc80a49fe0e0fea049a6d762eb76667498 + +COCOAPODS: 1.0.0.beta.3 diff --git a/Example/Pods/CocoaAsyncSocket/README.markdown b/Example/Pods/CocoaAsyncSocket/README.markdown new file mode 100644 index 0000000..c62119d --- /dev/null +++ b/Example/Pods/CocoaAsyncSocket/README.markdown @@ -0,0 +1,121 @@ +# CocoaAsyncSocket +[![Build Status](https://travis-ci.org/robbiehanson/CocoaAsyncSocket.svg?branch=master)](https://travis-ci.org/robbiehanson/CocoaAsyncSocket) [![Version Status](https://img.shields.io/cocoapods/v/CocoaAsyncSocket.svg?style=flat)](http://cocoadocs.org/docsets/CocoaAsyncSocket) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Platform](http://img.shields.io/cocoapods/p/CocoaAsyncSocket.svg?style=flat)](http://cocoapods.org/?q=CocoaAsyncSocket) [![license Public Domain](https://img.shields.io/badge/license-Public%20Domain-orange.svg?style=flat)](https://en.wikipedia.org/wiki/Public_domain) + + +CocoaAsyncSocket provides easy-to-use and powerful asynchronous socket libraries for Mac and iOS. The classes are described below. + +## Installation + +#### CocoaPods + +Install using [CocoaPods](http://cocoapods.org) by adding this line to your Podfile: + +````ruby +use_frameworks! # Add this if you are targeting iOS 8+ or using Swift +pod 'CocoaAsyncSocket' +```` + +#### Carthage + +CocoaAsyncSocket is [Carthage](https://github.com/Carthage/Carthage) compatible. To include it add the following line to your `Cartfile` + +```bash +github "robbiehanson/CocoaAsyncSocket" "master" +``` + +The project is currently configured to build for **iOS**, **tvOS** and **Mac**. After building with carthage the resultant frameworks will be stored in: + +* `Carthage/Build/iOS/CocoaAsyncSocket.framework` +* `Carthage/Build/tvOS/CocoaAsyncSocket.framework` +* `Carthage/Build/Mac/CocoaAsyncSocket.framework` + +Select the correct framework(s) and drag it into your project. + +#### Manual + +You can also include it into your project by adding the source files directly, but you should probably be using a dependency manager to keep up to date. + +### Importing + +Using Objective-C: + +```obj-c +@import CocoaAsyncSocket; // When using iOS 8+ frameworks +// OR +#import "CocoaAsyncSocket.h" // When not using frameworks, targeting iOS 7 or below +``` + +Using Swift: + +```swift +import CocoaAsyncSocket +``` + +## TCP + +**GCDAsyncSocket** and **AsyncSocket** are TCP/IP socket networking libraries. Here are the key features available in both: + +- Native objective-c, fully self-contained in one class.
+ _No need to muck around with sockets or streams. This class handles everything for you._ + +- Full delegate support
+ _Errors, connections, read completions, write completions, progress, and disconnections all result in a call to your delegate method._ + +- Queued non-blocking reads and writes, with optional timeouts.
+ _You tell it what to read or write, and it handles everything for you. Queueing, buffering, and searching for termination sequences within the stream - all handled for you automatically._ + +- Automatic socket acceptance.
+ _Spin up a server socket, tell it to accept connections, and it will call you with new instances of itself for each connection._ + +- Support for TCP streams over IPv4 and IPv6.
+ _Automatically connect to IPv4 or IPv6 hosts. Automatically accept incoming connections over both IPv4 and IPv6 with a single instance of this class. No more worrying about multiple sockets._ + +- Support for TLS / SSL
+ _Secure your socket with ease using just a single method call. Available for both client and server sockets._ + +**GCDAsyncSocket** is built atop Grand Central Dispatch: + +- Fully GCD based and Thread-Safe
+ _It runs entirely within its own GCD dispatch_queue, and is completely thread-safe. Further, the delegate methods are all invoked asynchronously onto a dispatch_queue of your choosing. This means parallel operation of your socket code, and your delegate/processing code._ + +- The Latest Technology & Performance Optimizations
+ _Internally the library takes advantage of technologies such as [kqueue's](http://en.wikipedia.org/wiki/Kqueue) to limit [system calls](http://en.wikipedia.org/wiki/System_call) and optimize buffer allocations. In other words, peak performance._ + +**AsyncSocket** wraps CFSocket and CFStream: + +- Fully Run-loop based
+ _Use it on the main thread or a worker thread. It plugs into the NSRunLoop with configurable modes._ + +## UDP + +**GCDAsyncUdpSocket** and **AsyncUdpSocket** are UDP/IP socket networking libraries. Here are the key features available in both: + +- Native objective-c, fully self-contained in one class.
+ _No need to muck around with low-level sockets. This class handles everything for you._ + +- Full delegate support.
+ _Errors, send completions, receive completions, and disconnections all result in a call to your delegate method._ + +- Queued non-blocking send and receive operations, with optional timeouts.
+ _You tell it what to send or receive, and it handles everything for you. Queueing, buffering, waiting and checking errno - all handled for you automatically._ + +- Support for IPv4 and IPv6.
+ _Automatically send/recv using IPv4 and/or IPv6. No more worrying about multiple sockets._ + +**GCDAsyncUdpSocket** is built atop Grand Central Dispatch: + +- Fully GCD based and Thread-Safe
+ _It runs entirely within its own GCD dispatch_queue, and is completely thread-safe. Further, the delegate methods are all invoked asynchronously onto a dispatch_queue of your choosing. This means parallel operation of your socket code, and your delegate/processing code._ + +**AsyncUdpSocket** wraps CFSocket: + +- Fully Run-loop based
+ _Use it on the main thread or a worker thread. It plugs into the NSRunLoop with configurable modes._ + +*** + +Can't find the answer to your question in any of the [wiki](https://github.com/robbiehanson/CocoaAsyncSocket/wiki) articles? Try the **[mailing list](http://groups.google.com/group/cocoaasyncsocket)**. +
+
+Love the project? Wanna buy me a coffee? (or a beer :D) [![donation](http://www.paypal.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=2M8C699FQ8AW2) + diff --git a/Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.h b/Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.h new file mode 100644 index 0000000..1cabc5a --- /dev/null +++ b/Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.h @@ -0,0 +1,1197 @@ +// +// GCDAsyncSocket.h +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q3 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import +#import +#import +#import +#import + +#include // AF_INET, AF_INET6 + +@class GCDAsyncReadPacket; +@class GCDAsyncWritePacket; +@class GCDAsyncSocketPreBuffer; + +extern NSString *const GCDAsyncSocketException; +extern NSString *const GCDAsyncSocketErrorDomain; + +extern NSString *const GCDAsyncSocketQueueName; +extern NSString *const GCDAsyncSocketThreadName; + +extern NSString *const GCDAsyncSocketManuallyEvaluateTrust; +#if TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketUseCFStreamForTLS; +#endif +#define GCDAsyncSocketSSLPeerName (NSString *)kCFStreamSSLPeerName +#define GCDAsyncSocketSSLCertificates (NSString *)kCFStreamSSLCertificates +#define GCDAsyncSocketSSLIsServer (NSString *)kCFStreamSSLIsServer +extern NSString *const GCDAsyncSocketSSLPeerID; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMin; +extern NSString *const GCDAsyncSocketSSLProtocolVersionMax; +extern NSString *const GCDAsyncSocketSSLSessionOptionFalseStart; +extern NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord; +extern NSString *const GCDAsyncSocketSSLCipherSuites; +#if !TARGET_OS_IPHONE +extern NSString *const GCDAsyncSocketSSLDiffieHellmanParameters; +#endif + +#define GCDAsyncSocketLoggingContext 65535 + + +typedef NS_ENUM(NSInteger, GCDAsyncSocketError) { + GCDAsyncSocketNoError = 0, // Never used + GCDAsyncSocketBadConfigError, // Invalid configuration + GCDAsyncSocketBadParamError, // Invalid parameter was passed + GCDAsyncSocketConnectTimeoutError, // A connect operation timed out + GCDAsyncSocketReadTimeoutError, // A read operation timed out + GCDAsyncSocketWriteTimeoutError, // A write operation timed out + GCDAsyncSocketReadMaxedOutError, // Reached set maxLength without completing + GCDAsyncSocketClosedError, // The remote peer closed the connection + GCDAsyncSocketOtherError, // Description provided in userInfo +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface GCDAsyncSocket : NSObject + +/** + * GCDAsyncSocket uses the standard delegate paradigm, + * but executes all delegate callbacks on a given delegate dispatch queue. + * This allows for maximum concurrency, while at the same time providing easy thread safety. + * + * You MUST set a delegate AND delegate dispatch queue before attempting to + * use the socket, or you will get an error. + * + * The socket queue is optional. + * If you pass NULL, GCDAsyncSocket will automatically create it's own socket queue. + * If you choose to provide a socket queue, the socket queue must not be a concurrent queue. + * If you choose to provide a socket queue, and the socket queue has a configured target queue, + * then please see the discussion for the method markSocketQueueTargetQueue. + * + * The delegate queue and socket queue can optionally be the same. +**/ +- (id)init; +- (id)initWithSocketQueue:(dispatch_queue_t)sq; +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq; +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq; + +#pragma mark Configuration + +@property (atomic, weak, readwrite) id delegate; +#if OS_OBJECT_USE_OBJC +@property (atomic, strong, readwrite) dispatch_queue_t delegateQueue; +#else +@property (atomic, assign, readwrite) dispatch_queue_t delegateQueue; +#endif + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr; +- (void)setDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; + +/** + * If you are setting the delegate to nil within the delegate's dealloc method, + * you may need to use the synchronous versions below. +**/ +- (void)synchronouslySetDelegate:(id)delegate; +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegate:(id)delegate delegateQueue:(dispatch_queue_t)delegateQueue; + +/** + * By default, both IPv4 and IPv6 are enabled. + * + * For accepting incoming connections, this means GCDAsyncSocket automatically supports both protocols, + * and can simulataneously accept incoming connections on either protocol. + * + * For outgoing connections, this means GCDAsyncSocket can connect to remote hosts running either protocol. + * If a DNS lookup returns only IPv4 results, GCDAsyncSocket will automatically use IPv4. + * If a DNS lookup returns only IPv6 results, GCDAsyncSocket will automatically use IPv6. + * If a DNS lookup returns both IPv4 and IPv6 results, the preferred protocol will be chosen. + * By default, the preferred protocol is IPv4, but may be configured as desired. +**/ + +@property (atomic, assign, readwrite, getter=isIPv4Enabled) BOOL IPv4Enabled; +@property (atomic, assign, readwrite, getter=isIPv6Enabled) BOOL IPv6Enabled; + +@property (atomic, assign, readwrite, getter=isIPv4PreferredOverIPv6) BOOL IPv4PreferredOverIPv6; + +/** + * User data allows you to associate arbitrary information with the socket. + * This data is not used internally by socket in any way. +**/ +@property (atomic, strong, readwrite) id userData; + +#pragma mark Accepting + +/** + * Tells the socket to begin listening and accepting connections on the given port. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) +**/ +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * This method is the same as acceptOnPort:error: with the + * additional option of specifying which interface to listen on. + * + * For example, you could specify that the socket should only accept connections over ethernet, + * and not other interfaces such as wifi. + * + * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept connections from the local machine. + * + * You can see the list of interfaces via the command line utility "ifconfig", + * or programmatically via the getifaddrs() function. + * + * To accept connections on any interface pass nil, or simply use the acceptOnPort:error: method. +**/ +- (BOOL)acceptOnInterface:(NSString *)interface port:(uint16_t)port error:(NSError **)errPtr; + +/** + * Tells the socket to begin listening and accepting connections on the unix domain at the given url. + * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, + * and the socket:didAcceptNewSocket: delegate method will be invoked. + * + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) + **/ +- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr; + +#pragma mark Connecting + +/** + * Connects to the given host and port. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: + * and uses the default interface, and no timeout. +**/ +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Connects to the given host and port with an optional timeout. + * + * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: and uses the default interface. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given host & port, via the optional interface, with an optional timeout. + * + * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * The host may also be the special strings "localhost" or "loopback" to specify connecting + * to a service on the local machine. + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + viaInterface:(NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given address, specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * This method invokes connectToAdd +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +/** + * This method is the same as connectToAddress:error: with an additional timeout option. + * To not time out use a negative time interval, or simply use the connectToAddress:error: method. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +/** + * Connects to the given address, using the specified interface and timeout. + * + * The address is specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * The interface may also be used to specify the local port (see below). + * + * The timeout is optional. To not time out use a negative time interval. + * + * This method will return NO if an error is detected, and set the error pointer (if one was given). + * Possible errors would be a nil host, invalid interface, or socket is already connected. + * + * If no errors are detected, this method will start a background connect operation and immediately return YES. + * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. + * + * Since this class supports queued reads and writes, you can immediately start reading and/or writing. + * All read/write operations will be queued, and upon socket connection, + * the operations will be dequeued and processed in order. + * + * The interface may optionally contain a port number at the end of the string, separated by a colon. + * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) + * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". + * To specify only local port: ":8082". + * Please note this is an advanced feature, and is somewhat hidden on purpose. + * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. + * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. + * Local ports do NOT need to match remote ports. In fact, they almost never do. + * This feature is here for networking professionals using very advanced techniques. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr + viaInterface:(NSString *)interface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; +/** + * Connects to the unix domain socket at the given url, using the specified timeout. + */ +- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +#pragma mark Disconnecting + +/** + * Disconnects immediately (synchronously). Any pending reads or writes are dropped. + * + * If the socket is not already disconnected, an invocation to the socketDidDisconnect:withError: delegate method + * will be queued onto the delegateQueue asynchronously (behind any previously queued delegate methods). + * In other words, the disconnected delegate method will be invoked sometime shortly after this method returns. + * + * Please note the recommended way of releasing a GCDAsyncSocket instance (e.g. in a dealloc method) + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket release]; + * + * If you plan on disconnecting the socket, and then immediately asking it to connect again, + * you'll likely want to do so like this: + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket setDelegate:self]; + * [asyncSocket connect...]; +**/ +- (void)disconnect; + +/** + * Disconnects after all pending reads have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending writes. +**/ +- (void)disconnectAfterReading; + +/** + * Disconnects after all pending writes have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending reads. +**/ +- (void)disconnectAfterWriting; + +/** + * Disconnects after all pending reads and writes have completed. + * After calling this, the read and write methods will do nothing. +**/ +- (void)disconnectAfterReadingAndWriting; + +#pragma mark Diagnostics + +/** + * Returns whether the socket is disconnected or connected. + * + * A disconnected socket may be recycled. + * That is, it can used again for connecting or listening. + * + * If a socket is in the process of connecting, it may be neither disconnected nor connected. +**/ +@property (atomic, readonly) BOOL isDisconnected; +@property (atomic, readonly) BOOL isConnected; + +/** + * Returns the local or remote host and port to which this socket is connected, or nil and 0 if not connected. + * The host will be an IP address. +**/ +@property (atomic, readonly) NSString *connectedHost; +@property (atomic, readonly) uint16_t connectedPort; +@property (atomic, readonly) NSURL *connectedUrl; + +@property (atomic, readonly) NSString *localHost; +@property (atomic, readonly) uint16_t localPort; + +/** + * Returns the local or remote address to which this socket is connected, + * specified as a sockaddr structure wrapped in a NSData object. + * + * @seealso connectedHost + * @seealso connectedPort + * @seealso localHost + * @seealso localPort +**/ +@property (atomic, readonly) NSData *connectedAddress; +@property (atomic, readonly) NSData *localAddress; + +/** + * Returns whether the socket is IPv4 or IPv6. + * An accepting socket may be both. +**/ +@property (atomic, readonly) BOOL isIPv4; +@property (atomic, readonly) BOOL isIPv6; + +/** + * Returns whether or not the socket has been secured via SSL/TLS. + * + * See also the startTLS method. +**/ +@property (atomic, readonly) BOOL isSecure; + +#pragma mark Reading + +// The readData and writeData methods won't block (they are asynchronous). +// +// When a read is complete the socket:didReadData:withTag: delegate method is dispatched on the delegateQueue. +// When a write is complete the socket:didWriteDataWithTag: delegate method is dispatched on the delegateQueue. +// +// You may optionally set a timeout for any read/write operation. (To not timeout, use a negative time interval.) +// If a read/write opertion times out, the corresponding "socket:shouldTimeout..." delegate method +// is called to optionally allow you to extend the timeout. +// Upon a timeout, the "socket:didDisconnectWithError:" method is called +// +// The tag is for your convenience. +// You can use it as an array index, step number, state id, pointer, etc. + +/** + * Reads the first available bytes that become available on the socket. + * + * If the timeout value is negative, the read operation will not use a timeout. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, the socket will create a buffer for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * A maximum of length bytes will be read. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * If maxLength is zero, no length restriction is enforced. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Reads the given number of bytes. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If the length is 0, this method does nothing and the delegate is not called. +**/ +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the given number of bytes. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If the length is 0, this method does nothing and the delegate is not called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. +**/ +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If you pass a maxLength parameter that is less than the length of the data parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass a maxLength parameter that is less than the length of the data (separator) parameter, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing (except maybe print a warning), and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while the socket is using it. + * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer via + * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * If you're developing your own custom protocol, be sure your separator can not occur naturally as + * part of the data between separators. + * For example, imagine you want to send several small documents over a socket. + * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. + * In this particular example, it would be better to use a protocol similar to HTTP with + * a header that includes the length of the document. + * Also be careful that your separator cannot occur naturally as part of the encoding for a character. + * + * The given data (separator) parameter should be immutable. + * For performance reasons, the socket will retain it, not copy it. + * So if it is immutable, don't modify it while the socket is using it. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Returns progress of the current read, from 0.0 to 1.0, or NaN if no current read (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr; + +#pragma mark Writing + +/** + * Writes data to the socket, and calls the delegate when finished. + * + * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called. + * If the timeout value is negative, the write operation will not use a timeout. + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is writing it. In other words, it's not safe to alter the data until after the delegate method + * socket:didWriteDataWithTag: is invoked signifying that this particular write operation has completed. + * This is due to the fact that GCDAsyncSocket does NOT copy the data. It simply retains it. + * This is for performance reasons. Often times, if NSMutableData is passed, it is because + * a request/response was built up in memory. Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes writing the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Returns progress of the current write, from 0.0 to 1.0, or NaN if no current write (use isnan() to check). + * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr; + +#pragma mark Security + +/** + * Secures the connection using SSL/TLS. + * + * This method may be called at any time, and the TLS handshake will occur after all pending reads and writes + * are finished. This allows one the option of sending a protocol dependent StartTLS message, and queuing + * the upgrade to TLS at the same time, without having to wait for the write to finish. + * Any reads or writes scheduled after this method is called will occur over the secured connection. + * + * ==== The available TOP-LEVEL KEYS are: + * + * - GCDAsyncSocketManuallyEvaluateTrust + * The value must be of type NSNumber, encapsulating a BOOL value. + * If you set this to YES, then the underlying SecureTransport system will not evaluate the SecTrustRef of the peer. + * Instead it will pause at the moment evaulation would typically occur, + * and allow us to handle the security evaluation however we see fit. + * So GCDAsyncSocket will invoke the delegate method socket:shouldTrustPeer: passing the SecTrustRef. + * + * Note that if you set this option, then all other configuration keys are ignored. + * Evaluation will be completely up to you during the socket:didReceiveTrust:completionHandler: delegate method. + * + * For more information on trust evaluation see: + * Apple's Technical Note TN2232 - HTTPS Server Trust Evaluation + * https://developer.apple.com/library/ios/technotes/tn2232/_index.html + * + * If unspecified, the default value is NO. + * + * - GCDAsyncSocketUseCFStreamForTLS (iOS only) + * The value must be of type NSNumber, encapsulating a BOOL value. + * By default GCDAsyncSocket will use the SecureTransport layer to perform encryption. + * This gives us more control over the security protocol (many more configuration options), + * plus it allows us to optimize things like sys calls and buffer allocation. + * + * However, if you absolutely must, you can instruct GCDAsyncSocket to use the old-fashioned encryption + * technique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocket + * will instead setup a CFRead/CFWriteStream. And then set the kCFStreamPropertySSLSettings property + * (via CFReadStreamSetProperty / CFWriteStreamSetProperty) and will pass the given options to this method. + * + * Thus all the other keys in the given dictionary will be ignored by GCDAsyncSocket, + * and will passed directly CFReadStreamSetProperty / CFWriteStreamSetProperty. + * For more infomation on these keys, please see the documentation for kCFStreamPropertySSLSettings. + * + * If unspecified, the default value is NO. + * + * ==== The available CONFIGURATION KEYS are: + * + * - kCFStreamSSLPeerName + * The value must be of type NSString. + * It should match the name in the X.509 certificate given by the remote party. + * See Apple's documentation for SSLSetPeerDomainName. + * + * - kCFStreamSSLCertificates + * The value must be of type NSArray. + * See Apple's documentation for SSLSetCertificate. + * + * - kCFStreamSSLIsServer + * The value must be of type NSNumber, encapsulationg a BOOL value. + * See Apple's documentation for SSLCreateContext for iOS. + * This is optional for iOS. If not supplied, a NO value is the default. + * This is not needed for Mac OS X, and the value is ignored. + * + * - GCDAsyncSocketSSLPeerID + * The value must be of type NSData. + * You must set this value if you want to use TLS session resumption. + * See Apple's documentation for SSLSetPeerID. + * + * - GCDAsyncSocketSSLProtocolVersionMin + * - GCDAsyncSocketSSLProtocolVersionMax + * The value(s) must be of type NSNumber, encapsulting a SSLProtocol value. + * See Apple's documentation for SSLSetProtocolVersionMin & SSLSetProtocolVersionMax. + * See also the SSLProtocol typedef. + * + * - GCDAsyncSocketSSLSessionOptionFalseStart + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionFalseStart. + * + * - GCDAsyncSocketSSLSessionOptionSendOneByteRecord + * The value must be of type NSNumber, encapsulating a BOOL value. + * See Apple's documentation for kSSLSessionOptionSendOneByteRecord. + * + * - GCDAsyncSocketSSLCipherSuites + * The values must be of type NSArray. + * Each item within the array must be a NSNumber, encapsulating + * See Apple's documentation for SSLSetEnabledCiphers. + * See also the SSLCipherSuite typedef. + * + * - GCDAsyncSocketSSLDiffieHellmanParameters (Mac OS X only) + * The value must be of type NSData. + * See Apple's documentation for SSLSetDiffieHellmanParams. + * + * ==== The following UNAVAILABLE KEYS are: (with throw an exception) + * + * - kCFStreamSSLAllowsAnyRoot (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsAnyRoot + * + * - kCFStreamSSLAllowsExpiredRoots (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredRoots + * + * - kCFStreamSSLAllowsExpiredCertificates (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetAllowsExpiredCerts + * + * - kCFStreamSSLValidatesCertificateChain (UNAVAILABLE) + * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). + * Corresponding deprecated method: SSLSetEnableCertVerify + * + * - kCFStreamSSLLevel (UNAVAILABLE) + * You MUST use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMin instead. + * Corresponding deprecated method: SSLSetProtocolVersionEnabled + * + * + * Please refer to Apple's documentation for corresponding SSLFunctions. + * + * If you pass in nil or an empty dictionary, the default settings will be used. + * + * IMPORTANT SECURITY NOTE: + * The default settings will check to make sure the remote party's certificate is signed by a + * trusted 3rd party certificate agency (e.g. verisign) and that the certificate is not expired. + * However it will not verify the name on the certificate unless you + * give it a name to verify against via the kCFStreamSSLPeerName key. + * The security implications of this are important to understand. + * Imagine you are attempting to create a secure connection to MySecureServer.com, + * but your socket gets directed to MaliciousServer.com because of a hacked DNS server. + * If you simply use the default settings, and MaliciousServer.com has a valid certificate, + * the default settings will not detect any problems since the certificate is valid. + * To properly secure your connection in this particular scenario you + * should set the kCFStreamSSLPeerName property to "MySecureServer.com". + * + * You can also perform additional validation in socketDidSecure. +**/ +- (void)startTLS:(NSDictionary *)tlsSettings; + +#pragma mark Advanced + +/** + * Traditionally sockets are not closed until the conversation is over. + * However, it is technically possible for the remote enpoint to close its write stream. + * Our socket would then be notified that there is no more data to be read, + * but our socket would still be writeable and the remote endpoint could continue to receive our data. + * + * The argument for this confusing functionality stems from the idea that a client could shut down its + * write stream after sending a request to the server, thus notifying the server there are to be no further requests. + * In practice, however, this technique did little to help server developers. + * + * To make matters worse, from a TCP perspective there is no way to tell the difference from a read stream close + * and a full socket close. They both result in the TCP stack receiving a FIN packet. The only way to tell + * is by continuing to write to the socket. If it was only a read stream close, then writes will continue to work. + * Otherwise an error will be occur shortly (when the remote end sends us a RST packet). + * + * In addition to the technical challenges and confusion, many high level socket/stream API's provide + * no support for dealing with the problem. If the read stream is closed, the API immediately declares the + * socket to be closed, and shuts down the write stream as well. In fact, this is what Apple's CFStream API does. + * It might sound like poor design at first, but in fact it simplifies development. + * + * The vast majority of the time if the read stream is closed it's because the remote endpoint closed its socket. + * Thus it actually makes sense to close the socket at this point. + * And in fact this is what most networking developers want and expect to happen. + * However, if you are writing a server that interacts with a plethora of clients, + * you might encounter a client that uses the discouraged technique of shutting down its write stream. + * If this is the case, you can set this property to NO, + * and make use of the socketDidCloseReadStream delegate method. + * + * The default value is YES. +**/ +@property (atomic, assign, readwrite) BOOL autoDisconnectOnClosedReadStream; + +/** + * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. + * In most cases, the instance creates this queue itself. + * However, to allow for maximum flexibility, the internal queue may be passed in the init method. + * This allows for some advanced options such as controlling socket priority via target queues. + * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. + * + * For example, imagine there are 2 queues: + * dispatch_queue_t socketQueue; + * dispatch_queue_t socketTargetQueue; + * + * If you do this (pseudo-code): + * socketQueue.targetQueue = socketTargetQueue; + * + * Then all socketQueue operations will actually get run on the given socketTargetQueue. + * This is fine and works great in most situations. + * But if you run code directly from within the socketTargetQueue that accesses the socket, + * you could potentially get deadlock. Imagine the following code: + * + * - (BOOL)socketHasSomething + * { + * __block BOOL result = NO; + * dispatch_block_t block = ^{ + * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; + * } + * if (is_executing_on_queue(socketQueue)) + * block(); + * else + * dispatch_sync(socketQueue, block); + * + * return result; + * } + * + * What happens if you call this method from the socketTargetQueue? The result is deadlock. + * This is because the GCD API offers no mechanism to discover a queue's targetQueue. + * Thus we have no idea if our socketQueue is configured with a targetQueue. + * If we had this information, we could easily avoid deadlock. + * But, since these API's are missing or unfeasible, you'll have to explicitly set it. + * + * IF you pass a socketQueue via the init method, + * AND you've configured the passed socketQueue with a targetQueue, + * THEN you should pass the end queue in the target hierarchy. + * + * For example, consider the following queue hierarchy: + * socketQueue -> ipQueue -> moduleQueue + * + * This example demonstrates priority shaping within some server. + * All incoming client connections from the same IP address are executed on the same target queue. + * And all connections for a particular module are executed on the same target queue. + * Thus, the priority of all networking for the entire module can be changed on the fly. + * Additionally, networking traffic from a single IP cannot monopolize the module. + * + * Here's how you would accomplish something like that: + * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock + * { + * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); + * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; + * + * dispatch_set_target_queue(socketQueue, ipQueue); + * dispatch_set_target_queue(iqQueue, moduleQueue); + * + * return socketQueue; + * } + * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket + * { + * [clientConnections addObject:newSocket]; + * [newSocket markSocketQueueTargetQueue:moduleQueue]; + * } + * + * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. + * This is often NOT the case, as such queues are used solely for execution shaping. +**/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; + +/** + * It's not thread-safe to access certain variables from outside the socket's internal queue. + * + * For example, the socket file descriptor. + * File descriptors are simply integers which reference an index in the per-process file table. + * However, when one requests a new file descriptor (by opening a file or socket), + * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. + * So if we're not careful, the following could be possible: + * + * - Thread A invokes a method which returns the socket's file descriptor. + * - The socket is closed via the socket's internal queue on thread B. + * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. + * - Thread A is now accessing/altering the file instead of the socket. + * + * In addition to this, other variables are not actually objects, + * and thus cannot be retained/released or even autoreleased. + * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. + * + * Although there are internal variables that make it difficult to maintain thread-safety, + * it is important to provide access to these variables + * to ensure this class can be used in a wide array of environments. + * This method helps to accomplish this by invoking the current block on the socket's internal queue. + * The methods below can be invoked from within the block to access + * those generally thread-unsafe internal variables in a thread-safe manner. + * The given block will be invoked synchronously on the socket's internal queue. + * + * If you save references to any protected variables and use them outside the block, you do so at your own peril. +**/ +- (void)performBlock:(dispatch_block_t)block; + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's file descriptor(s). + * If the socket is a server socket (is accepting incoming connections), + * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. +**/ +- (int)socketFD; +- (int)socket4FD; +- (int)socket6FD; + +#if TARGET_OS_IPHONE + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's internal CFReadStream/CFWriteStream. + * + * These streams are only used as workarounds for specific iOS shortcomings: + * + * - Apple has decided to keep the SecureTransport framework private is iOS. + * This means the only supplied way to do SSL/TLS is via CFStream or some other API layered on top of it. + * Thus, in order to provide SSL/TLS support on iOS we are forced to rely on CFStream, + * instead of the preferred and faster and more powerful SecureTransport. + * + * - If a socket doesn't have backgrounding enabled, and that socket is closed while the app is backgrounded, + * Apple only bothers to notify us via the CFStream API. + * The faster and more powerful GCD API isn't notified properly in this case. + * + * See also: (BOOL)enableBackgroundingOnSocket +**/ +- (CFReadStreamRef)readStream; +- (CFWriteStreamRef)writeStream; + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Configures the socket to allow it to operate when the iOS application has been backgrounded. + * In other words, this method creates a read & write stream, and invokes: + * + * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * + * Returns YES if successful, NO otherwise. + * + * Note: Apple does not officially support backgrounding server sockets. + * That is, if your socket is accepting incoming connections, Apple does not officially support + * allowing iOS applications to accept incoming connections while an app is backgrounded. + * + * Example usage: + * + * - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port + * { + * [asyncSocket performBlock:^{ + * [asyncSocket enableBackgroundingOnSocket]; + * }]; + * } +**/ +- (BOOL)enableBackgroundingOnSocket; + +#endif + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's SSLContext, if SSL/TLS has been started on the socket. +**/ +- (SSLContextRef)sslContext; + +#pragma mark Utilities + +/** + * The address lookup utility used by the class. + * This method is synchronous, so it's recommended you use it on a background thread/queue. + * + * The special strings "localhost" and "loopback" return the loopback address for IPv4 and IPv6. + * + * @returns + * A mutable array with all IPv4 and IPv6 addresses returned by getaddrinfo. + * The addresses are specifically for TCP connections. + * You can filter the addresses, if needed, using the other utility methods provided by the class. +**/ ++ (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr; + +/** + * Extracting host and port information from raw address data. +**/ + ++ (NSString *)hostFromAddress:(NSData *)address; ++ (uint16_t)portFromAddress:(NSData *)address; + ++ (BOOL)isIPv4Address:(NSData *)address; ++ (BOOL)isIPv6Address:(NSData *)address; + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address; + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_t *)afPtr fromAddress:(NSData *)address; + +/** + * A few common line separators, for use with the readDataToData:... methods. +**/ ++ (NSData *)CRLFData; // 0x0D0A ++ (NSData *)CRData; // 0x0D ++ (NSData *)LFData; // 0x0A ++ (NSData *)ZeroData; // 0x00 + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol GCDAsyncSocketDelegate +@optional + +/** + * This method is called immediately prior to socket:didAcceptNewSocket:. + * It optionally allows a listening socket to specify the socketQueue for a new accepted socket. + * If this method is not implemented, or returns NULL, the new accepted socket will create its own default queue. + * + * Since you cannot autorelease a dispatch_queue, + * this method uses the "new" prefix in its name to specify that the returned queue has been retained. + * + * Thus you could do something like this in the implementation: + * return dispatch_queue_create("MyQueue", NULL); + * + * If you are placing multiple sockets on the same queue, + * then care should be taken to increment the retain count each time this method is invoked. + * + * For example, your implementation might look something like this: + * dispatch_retain(myExistingQueue); + * return myExistingQueue; +**/ +- (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock; + +/** + * Called when a socket accepts a connection. + * Another socket is automatically spawned to handle it. + * + * You must retain the newSocket if you wish to handle the connection. + * Otherwise the newSocket instance will be released and the spawned connection will be closed. + * + * By default the new socket will have the same delegate and delegateQueue. + * You may, of course, change this at any time. +**/ +- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. +**/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. + **/ +- (void)socket:(GCDAsyncSocket *)sock didConnectToUrl:(NSURL *)url; + +/** + * Called when a socket has completed reading the requested data into memory. + * Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag; + +/** + * Called when a socket has read in data, but has not yet completed the read. + * This would occur if using readToData: or readToLength: methods. + * It may be used to for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called when a socket has completed writing the requested data. Not called if there is an error. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag; + +/** + * Called when a socket has written some data, but has not yet completed the entire write. + * It may be used to for things such as updating progress bars. +**/ +- (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called if a read operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been read so far for the read operation. + * + * Note that this method may be called multiple times for a single read if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Called if a write operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been written so far for the write operation. + * + * Note that this method may be called multiple times for a single write if you return positive numbers. +**/ +- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutWriteWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Conditionally called if the read stream closes, but the write stream may still be writeable. + * + * This delegate method is only called if autoDisconnectOnClosedReadStream has been set to NO. + * See the discussion on the autoDisconnectOnClosedReadStream method for more information. +**/ +- (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock; + +/** + * Called when a socket disconnects with or without error. + * + * If you call the disconnect method, and the socket wasn't already disconnected, + * then an invocation of this delegate method will be enqueued on the delegateQueue + * before the disconnect method returns. + * + * Note: If the GCDAsyncSocket instance is deallocated while it is still connected, + * and the delegate is not also deallocated, then this method will be invoked, + * but the sock parameter will be nil. (It must necessarily be nil since it is no longer available.) + * This is a generally rare, but is possible if one writes code like this: + * + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * In this case it may preferrable to nil the delegate beforehand, like this: + * + * asyncSocket.delegate = nil; // Don't invoke my delegate method + * asyncSocket = nil; // I'm implicitly disconnecting the socket + * + * Of course, this depends on how your state machine is configured. +**/ +- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err; + +/** + * Called after the socket has successfully completed SSL/TLS negotiation. + * This method is not called unless you use the provided startTLS method. + * + * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close, + * and the socketDidDisconnect:withError: delegate method will be called with the specific SSL error code. +**/ +- (void)socketDidSecure:(GCDAsyncSocket *)sock; + +/** + * Allows a socket delegate to hook into the TLS handshake and manually validate the peer it's connecting to. + * + * This is only called if startTLS is invoked with options that include: + * - GCDAsyncSocketManuallyEvaluateTrust == YES + * + * Typically the delegate will use SecTrustEvaluate (and related functions) to properly validate the peer. + * + * Note from Apple's documentation: + * Because [SecTrustEvaluate] might look on the network for certificates in the certificate chain, + * [it] might block while attempting network access. You should never call it from your main thread; + * call it only from within a function running on a dispatch queue or on a separate thread. + * + * Thus this method uses a completionHandler block rather than a normal return value. + * The completionHandler block is thread-safe, and may be invoked from a background queue/thread. + * It is safe to invoke the completionHandler block even if the socket has been closed. +**/ +- (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust + completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler; + +@end diff --git a/Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m b/Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m new file mode 100644 index 0000000..248118f --- /dev/null +++ b/Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m @@ -0,0 +1,8239 @@ +// +// GCDAsyncSocket.m +// +// This class is in the public domain. +// Originally created by Robbie Hanson in Q4 2010. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import "GCDAsyncSocket.h" + +#if TARGET_OS_IPHONE +#import +#endif + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +// For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC +#endif + + +#ifndef GCDAsyncSocketLoggingEnabled +#define GCDAsyncSocketLoggingEnabled 0 +#endif + +#if GCDAsyncSocketLoggingEnabled + +// Logging Enabled - See log level below + +// Logging uses the CocoaLumberjack framework (which is also GCD based). +// https://github.com/robbiehanson/CocoaLumberjack +// +// It allows us to do a lot of logging without significantly slowing down the code. +#import "DDLog.h" + +#define LogAsync YES +#define LogContext GCDAsyncSocketLoggingContext + +#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) +#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) + +#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) +#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__) + +#ifndef GCDAsyncSocketLogLevel +#define GCDAsyncSocketLogLevel LOG_LEVEL_VERBOSE +#endif + +// Log levels : off, error, warn, info, verbose +static const int logLevel = GCDAsyncSocketLogLevel; + +#else + +// Logging Disabled + +#define LogError(frmt, ...) {} +#define LogWarn(frmt, ...) {} +#define LogInfo(frmt, ...) {} +#define LogVerbose(frmt, ...) {} + +#define LogCError(frmt, ...) {} +#define LogCWarn(frmt, ...) {} +#define LogCInfo(frmt, ...) {} +#define LogCVerbose(frmt, ...) {} + +#define LogTrace() {} +#define LogCTrace(frmt, ...) {} + +#endif + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. +**/ +#define return_from_block return + +/** + * A socket file descriptor is really just an integer. + * It represents the index of the socket within the kernel. + * This makes invalid file descriptor comparisons easier to read. +**/ +#define SOCKET_NULL -1 + + +NSString *const GCDAsyncSocketException = @"GCDAsyncSocketException"; +NSString *const GCDAsyncSocketErrorDomain = @"GCDAsyncSocketErrorDomain"; + +NSString *const GCDAsyncSocketQueueName = @"GCDAsyncSocket"; +NSString *const GCDAsyncSocketThreadName = @"GCDAsyncSocket-CFStream"; + +NSString *const GCDAsyncSocketManuallyEvaluateTrust = @"GCDAsyncSocketManuallyEvaluateTrust"; +#if TARGET_OS_IPHONE +NSString *const GCDAsyncSocketUseCFStreamForTLS = @"GCDAsyncSocketUseCFStreamForTLS"; +#endif +NSString *const GCDAsyncSocketSSLPeerID = @"GCDAsyncSocketSSLPeerID"; +NSString *const GCDAsyncSocketSSLProtocolVersionMin = @"GCDAsyncSocketSSLProtocolVersionMin"; +NSString *const GCDAsyncSocketSSLProtocolVersionMax = @"GCDAsyncSocketSSLProtocolVersionMax"; +NSString *const GCDAsyncSocketSSLSessionOptionFalseStart = @"GCDAsyncSocketSSLSessionOptionFalseStart"; +NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord = @"GCDAsyncSocketSSLSessionOptionSendOneByteRecord"; +NSString *const GCDAsyncSocketSSLCipherSuites = @"GCDAsyncSocketSSLCipherSuites"; +#if !TARGET_OS_IPHONE +NSString *const GCDAsyncSocketSSLDiffieHellmanParameters = @"GCDAsyncSocketSSLDiffieHellmanParameters"; +#endif + +enum GCDAsyncSocketFlags +{ + kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting) + kConnected = 1 << 1, // If set, the socket is connected + kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed + kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout + kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout + kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued + kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued + kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown. + kReadSourceSuspended = 1 << 8, // If set, the read source is suspended + kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended + kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS + kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete + kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete + kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS + kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket + kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained + kDealloc = 1 << 16, // If set, the socket is being deallocated +#if TARGET_OS_IPHONE + kAddedStreamsToRunLoop = 1 << 17, // If set, CFStreams have been added to listener thread + kUsingCFStreamForTLS = 1 << 18, // If set, we're forced to use CFStream instead of SecureTransport + kSecureSocketHasBytesAvailable = 1 << 19, // If set, CFReadStream has notified us of bytes available +#endif +}; + +enum GCDAsyncSocketConfig +{ + kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled + kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled + kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4 + kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes +}; + +#if TARGET_OS_IPHONE + static NSThread *cfstreamThread; // Used for CFStreams + + + static uint64_t cfstreamThreadRetainCount; // setup & teardown + static dispatch_queue_t cfstreamThreadSetupQueue; // setup & teardown +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * A PreBuffer is used when there is more data available on the socket + * than is being requested by current read request. + * In this case we slurp up all data from the socket (to minimize sys calls), + * and store additional yet unread data in a "prebuffer". + * + * The prebuffer is entirely drained before we read from the socket again. + * In other words, a large chunk of data is written is written to the prebuffer. + * The prebuffer is then drained via a series of one or more reads (for subsequent read request(s)). + * + * A ring buffer was once used for this purpose. + * But a ring buffer takes up twice as much memory as needed (double the size for mirroring). + * In fact, it generally takes up more than twice the needed size as everything has to be rounded up to vm_page_size. + * And since the prebuffer is always completely drained after being written to, a full ring buffer isn't needed. + * + * The current design is very simple and straight-forward, while also keeping memory requirements lower. +**/ + +@interface GCDAsyncSocketPreBuffer : NSObject +{ + uint8_t *preBuffer; + size_t preBufferSize; + + uint8_t *readPointer; + uint8_t *writePointer; +} + +- (id)initWithCapacity:(size_t)numBytes; + +- (void)ensureCapacityForWrite:(size_t)numBytes; + +- (size_t)availableBytes; +- (uint8_t *)readBuffer; + +- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr; + +- (size_t)availableSpace; +- (uint8_t *)writeBuffer; + +- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr; + +- (void)didRead:(size_t)bytesRead; +- (void)didWrite:(size_t)bytesWritten; + +- (void)reset; + +@end + +@implementation GCDAsyncSocketPreBuffer + +- (id)initWithCapacity:(size_t)numBytes +{ + if ((self = [super init])) + { + preBufferSize = numBytes; + preBuffer = malloc(preBufferSize); + + readPointer = preBuffer; + writePointer = preBuffer; + } + return self; +} + +- (void)dealloc +{ + if (preBuffer) + free(preBuffer); +} + +- (void)ensureCapacityForWrite:(size_t)numBytes +{ + size_t availableSpace = [self availableSpace]; + + if (numBytes > availableSpace) + { + size_t additionalBytes = numBytes - availableSpace; + + size_t newPreBufferSize = preBufferSize + additionalBytes; + uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize); + + size_t readPointerOffset = readPointer - preBuffer; + size_t writePointerOffset = writePointer - preBuffer; + + preBuffer = newPreBuffer; + preBufferSize = newPreBufferSize; + + readPointer = preBuffer + readPointerOffset; + writePointer = preBuffer + writePointerOffset; + } +} + +- (size_t)availableBytes +{ + return writePointer - readPointer; +} + +- (uint8_t *)readBuffer +{ + return readPointer; +} + +- (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr +{ + if (bufferPtr) *bufferPtr = readPointer; + if (availableBytesPtr) *availableBytesPtr = [self availableBytes]; +} + +- (void)didRead:(size_t)bytesRead +{ + readPointer += bytesRead; + + if (readPointer == writePointer) + { + // The prebuffer has been drained. Reset pointers. + readPointer = preBuffer; + writePointer = preBuffer; + } +} + +- (size_t)availableSpace +{ + return preBufferSize - (writePointer - preBuffer); +} + +- (uint8_t *)writeBuffer +{ + return writePointer; +} + +- (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr +{ + if (bufferPtr) *bufferPtr = writePointer; + if (availableSpacePtr) *availableSpacePtr = [self availableSpace]; +} + +- (void)didWrite:(size_t)bytesWritten +{ + writePointer += bytesWritten; +} + +- (void)reset +{ + readPointer = preBuffer; + writePointer = preBuffer; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncReadPacket encompasses the instructions for any given read. + * The content of a read packet allows the code to determine if we're: + * - reading to a certain length + * - reading to a certain separator + * - or simply reading the first chunk of available data +**/ +@interface GCDAsyncReadPacket : NSObject +{ + @public + NSMutableData *buffer; + NSUInteger startOffset; + NSUInteger bytesDone; + NSUInteger maxLength; + NSTimeInterval timeout; + NSUInteger readLength; + NSData *term; + BOOL bufferOwner; + NSUInteger originalBufferLength; + long tag; +} +- (id)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i; + +- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead; + +- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr; + +- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable; +- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr; +- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr; + +- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes; + +@end + +@implementation GCDAsyncReadPacket + +- (id)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i +{ + if((self = [super init])) + { + bytesDone = 0; + maxLength = m; + timeout = t; + readLength = l; + term = [e copy]; + tag = i; + + if (d) + { + buffer = d; + startOffset = s; + bufferOwner = NO; + originalBufferLength = [d length]; + } + else + { + if (readLength > 0) + buffer = [[NSMutableData alloc] initWithLength:readLength]; + else + buffer = [[NSMutableData alloc] initWithLength:0]; + + startOffset = 0; + bufferOwner = YES; + originalBufferLength = 0; + } + } + return self; +} + +/** + * Increases the length of the buffer (if needed) to ensure a read of the given size will fit. +**/ +- (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead +{ + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (bytesToRead > buffSpace) + { + NSUInteger buffInc = bytesToRead - buffSpace; + + [buffer increaseLengthBy:buffInc]; + } +} + +/** + * This method is used when we do NOT know how much data is available to be read from the socket. + * This method returns the default value unless it exceeds the specified readLength or maxLength. + * + * Furthermore, the shouldPreBuffer decision is based upon the packet type, + * and whether the returned value would fit in the current buffer without requiring a resize of the buffer. +**/ +- (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr +{ + NSUInteger result; + + if (readLength > 0) + { + // Read a specific length of data + + result = MIN(defaultValue, (readLength - bytesDone)); + + // There is no need to prebuffer since we know exactly how much data we need to read. + // Even if the buffer isn't currently big enough to fit this amount of data, + // it would have to be resized eventually anyway. + + if (shouldPreBufferPtr) + *shouldPreBufferPtr = NO; + } + else + { + // Either reading until we find a specified terminator, + // or we're simply reading all available data. + // + // In other words, one of: + // + // - readDataToData packet + // - readDataWithTimeout packet + + if (maxLength > 0) + result = MIN(defaultValue, (maxLength - bytesDone)); + else + result = defaultValue; + + // Since we don't know the size of the read in advance, + // the shouldPreBuffer decision is based upon whether the returned value would fit + // in the current buffer without requiring a resize of the buffer. + // + // This is because, in all likelyhood, the amount read from the socket will be less than the default value. + // Thus we should avoid over-allocating the read buffer when we can simply use the pre-buffer instead. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + NSUInteger buffSpace = buffSize - buffUsed; + + if (buffSpace >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + } + + return result; +} + +/** + * For read packets without a set terminator, returns the amount of data + * that can be read without exceeding the readLength or maxLength. + * + * The given parameter indicates the number of bytes estimated to be available on the socket, + * which is taken into consideration during the calculation. + * + * The given hint MUST be greater than zero. +**/ +- (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable +{ + NSAssert(term == nil, @"This method does not apply to term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + if (readLength > 0) + { + // Read a specific length of data + + return MIN(bytesAvailable, (readLength - bytesDone)); + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read a certain length of data that exceeds the size of the buffer, + // then it is clear that our code will resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + } + else + { + // Read all available data + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // No need to avoid resizing the buffer. + // If the user provided their own buffer, + // and told us to read all available data without giving us a maxLength, + // then it is clear that our code might resize the buffer during the read operation. + // + // This method does not actually do any resizing. + // The resizing will happen elsewhere if needed. + + return result; + } +} + +/** + * For read packets with a set terminator, returns the amount of data + * that can be read without exceeding the maxLength. + * + * The given parameter indicates the number of bytes estimated to be available on the socket, + * which is taken into consideration during the calculation. + * + * To optimize memory allocations, mem copies, and mem moves + * the shouldPreBuffer boolean value will indicate if the data should be read into a prebuffer first, + * or if the data can be read directly into the read packet's buffer. +**/ +- (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); + + + NSUInteger result = bytesAvailable; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + // Should the data be read into the read packet's buffer, or into a pre-buffer first? + // + // One would imagine the preferred option is the faster one. + // So which one is faster? + // + // Reading directly into the packet's buffer requires: + // 1. Possibly resizing packet buffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Possibly copying overflow into prebuffer (malloc/realloc, memcpy) + // + // Reading into prebuffer first: + // 1. Possibly resizing prebuffer (malloc/realloc) + // 2. Filling buffer (read) + // 3. Searching for term (memcmp) + // 4. Copying underflow into packet buffer (malloc/realloc, memcpy) + // 5. Removing underflow from prebuffer (memmove) + // + // Comparing the performance of the two we can see that reading + // data into the prebuffer first is slower due to the extra memove. + // + // However: + // The implementation of NSMutableData is open source via core foundation's CFMutableData. + // Decreasing the length of a mutable data object doesn't cause a realloc. + // In other words, the capacity of a mutable data object can grow, but doesn't shrink. + // + // This means the prebuffer will rarely need a realloc. + // The packet buffer, on the other hand, may often need a realloc. + // This is especially true if we are the buffer owner. + // Furthermore, if we are constantly realloc'ing the packet buffer, + // and then moving the overflow into the prebuffer, + // then we're consistently over-allocating memory for each term read. + // And now we get into a bit of a tradeoff between speed and memory utilization. + // + // The end result is that the two perform very similarly. + // And we can answer the original question very simply by another means. + // + // If we can read all the data directly into the packet's buffer without resizing it first, + // then we do so. Otherwise we use the prebuffer. + + if (shouldPreBufferPtr) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffUsed = startOffset + bytesDone; + + if ((buffSize - buffUsed) >= result) + *shouldPreBufferPtr = NO; + else + *shouldPreBufferPtr = YES; + } + + return result; +} + +/** + * For read packets with a set terminator, + * returns the amount of data that can be read from the given preBuffer, + * without going over a terminator or the maxLength. + * + * It is assumed the terminator has not already been read. +**/ +- (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert([preBuffer availableBytes] > 0, @"Invoked with empty pre buffer!"); + + // We know that the terminator, as a whole, doesn't exist in our own buffer. + // But it is possible that a _portion_ of it exists in our buffer. + // So we're going to look for the terminator starting with a portion of our own buffer. + // + // Example: + // + // term length = 3 bytes + // bytesDone = 5 bytes + // preBuffer length = 5 bytes + // + // If we append the preBuffer to our buffer, + // it would look like this: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // --------------------- + // + // So we start our search here: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // -------^-^-^--------- + // + // And move forwards... + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------^-^-^------- + // + // Until we find the terminator or reach the end. + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------------^-^-^- + + BOOL found = NO; + + NSUInteger termLength = [term length]; + NSUInteger preBufferLength = [preBuffer availableBytes]; + + if ((bytesDone + preBufferLength) < termLength) + { + // Not enough data for a full term sequence yet + return preBufferLength; + } + + NSUInteger maxPreBufferLength; + if (maxLength > 0) { + maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone)); + + // Note: maxLength >= termLength + } + else { + maxPreBufferLength = preBufferLength; + } + + uint8_t seq[termLength]; + const void *termBuf = [term bytes]; + + NSUInteger bufLen = MIN(bytesDone, (termLength - 1)); + uint8_t *buf = (uint8_t *)[buffer mutableBytes] + startOffset + bytesDone - bufLen; + + NSUInteger preLen = termLength - bufLen; + const uint8_t *pre = [preBuffer readBuffer]; + + NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above. + + NSUInteger result = maxPreBufferLength; + + NSUInteger i; + for (i = 0; i < loopCount; i++) + { + if (bufLen > 0) + { + // Combining bytes from buffer and preBuffer + + memcpy(seq, buf, bufLen); + memcpy(seq + bufLen, pre, preLen); + + if (memcmp(seq, termBuf, termLength) == 0) + { + result = preLen; + found = YES; + break; + } + + buf++; + bufLen--; + preLen++; + } + else + { + // Comparing directly from preBuffer + + if (memcmp(pre, termBuf, termLength) == 0) + { + NSUInteger preOffset = pre - [preBuffer readBuffer]; // pointer arithmetic + + result = preOffset + termLength; + found = YES; + break; + } + + pre++; + } + } + + // There is no need to avoid resizing the buffer in this particular situation. + + if (foundPtr) *foundPtr = found; + return result; +} + +/** + * For read packets with a set terminator, scans the packet buffer for the term. + * It is assumed the terminator had not been fully read prior to the new bytes. + * + * If the term is found, the number of excess bytes after the term are returned. + * If the term is not found, this method will return -1. + * + * Note: A return value of zero means the term was found at the very end. + * + * Prerequisites: + * The given number of bytes have been added to the end of our buffer. + * Our bytesDone variable has NOT been changed due to the prebuffered bytes. +**/ +- (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + + // The implementation of this method is very similar to the above method. + // See the above method for a discussion of the algorithm used here. + + uint8_t *buff = [buffer mutableBytes]; + NSUInteger buffLength = bytesDone + numBytes; + + const void *termBuff = [term bytes]; + NSUInteger termLength = [term length]; + + // Note: We are dealing with unsigned integers, + // so make sure the math doesn't go below zero. + + NSUInteger i = ((buffLength - numBytes) >= termLength) ? (buffLength - numBytes - termLength + 1) : 0; + + while (i + termLength <= buffLength) + { + uint8_t *subBuffer = buff + startOffset + i; + + if (memcmp(subBuffer, termBuff, termLength) == 0) + { + return buffLength - (i + termLength); + } + + i++; + } + + return -1; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncWritePacket encompasses the instructions for any given write. +**/ +@interface GCDAsyncWritePacket : NSObject +{ + @public + NSData *buffer; + NSUInteger bytesDone; + long tag; + NSTimeInterval timeout; +} +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i; +@end + +@implementation GCDAsyncWritePacket + +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i +{ + if((self = [super init])) + { + buffer = d; // Retain not copy. For performance as documented in header file. + bytesDone = 0; + timeout = t; + tag = i; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncSpecialPacket encompasses special instructions for interruptions in the read/write queues. + * This class my be altered to support more than just TLS in the future. +**/ +@interface GCDAsyncSpecialPacket : NSObject +{ + @public + NSDictionary *tlsSettings; +} +- (id)initWithTLSSettings:(NSDictionary *)settings; +@end + +@implementation GCDAsyncSpecialPacket + +- (id)initWithTLSSettings:(NSDictionary *)settings +{ + if((self = [super init])) + { + tlsSettings = [settings copy]; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDAsyncSocket +{ + uint32_t flags; + uint16_t config; + + __weak id delegate; + dispatch_queue_t delegateQueue; + + int socket4FD; + int socket6FD; + int socketUN; + NSURL *socketUrl; + int stateIndex; + NSData * connectInterface4; + NSData * connectInterface6; + NSData * connectInterfaceUN; + + dispatch_queue_t socketQueue; + + dispatch_source_t accept4Source; + dispatch_source_t accept6Source; + dispatch_source_t acceptUNSource; + dispatch_source_t connectTimer; + dispatch_source_t readSource; + dispatch_source_t writeSource; + dispatch_source_t readTimer; + dispatch_source_t writeTimer; + + NSMutableArray *readQueue; + NSMutableArray *writeQueue; + + GCDAsyncReadPacket *currentRead; + GCDAsyncWritePacket *currentWrite; + + unsigned long socketFDBytesAvailable; + + GCDAsyncSocketPreBuffer *preBuffer; + +#if TARGET_OS_IPHONE + CFStreamClientContext streamContext; + CFReadStreamRef readStream; + CFWriteStreamRef writeStream; +#endif + SSLContextRef sslContext; + GCDAsyncSocketPreBuffer *sslPreBuffer; + size_t sslWriteCachedLength; + OSStatus sslErrCode; + OSStatus lastSSLHandshakeError; + + void *IsOnSocketQueueOrTargetQueueKey; + + id userData; +} + +- (id)init +{ + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; +} + +- (id)initWithSocketQueue:(dispatch_queue_t)sq +{ + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq +{ + return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; +} + +- (id)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq +{ + if((self = [super init])) + { + delegate = aDelegate; + delegateQueue = dq; + + #if !OS_OBJECT_USE_OBJC + if (dq) dispatch_retain(dq); + #endif + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + socketUN = SOCKET_NULL; + socketUrl = nil; + stateIndex = 0; + + if (sq) + { + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + + socketQueue = sq; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(sq); + #endif + } + else + { + socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL); + } + + // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. + // From the documentation: + // + // > Keys are only compared as pointers and are never dereferenced. + // > Thus, you can use a pointer to a static variable for a specific subsystem or + // > any other value that allows you to identify the value uniquely. + // + // We're just going to use the memory address of an ivar. + // Specifically an ivar that is explicitly named for our purpose to make the code more readable. + // + // However, it feels tedious (and less readable) to include the "&" all the time: + // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) + // + // So we're going to make it so it doesn't matter if we use the '&' or not, + // by assigning the value of the ivar to the address of the ivar. + // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; + + IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; + + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); + + readQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentRead = nil; + + writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; + currentWrite = nil; + + preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + } + return self; +} + +- (void)dealloc +{ + LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); + + // Set dealloc flag. + // This is used by closeWithError to ensure we don't accidentally retain ourself. + flags |= kDealloc; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + [self closeWithError:nil]; + } + else + { + dispatch_sync(socketQueue, ^{ + [self closeWithError:nil]; + }); + } + + delegate = nil; + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + #endif + delegateQueue = NULL; + + #if !OS_OBJECT_USE_OBJC + if (socketQueue) dispatch_release(socketQueue); + #endif + socketQueue = NULL; + + LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)delegate +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegate; + } + else + { + __block id result; + + dispatch_sync(socketQueue, ^{ + result = delegate; + }); + + return result; + } +} + +- (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + delegate = newDelegate; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate +{ + [self setDelegate:newDelegate synchronously:YES]; +} + +- (dispatch_queue_t)delegateQueue +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegateQueue; + } + else + { + __block dispatch_queue_t result; + + dispatch_sync(socketQueue, ^{ + result = delegateQueue; + }); + + return result; + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:YES]; +} + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (delegatePtr) *delegatePtr = delegate; + if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; + } + else + { + __block id dPtr = NULL; + __block dispatch_queue_t dqPtr = NULL; + + dispatch_sync(socketQueue, ^{ + dPtr = delegate; + dqPtr = delegateQueue; + }); + + if (delegatePtr) *delegatePtr = dPtr; + if (delegateQueuePtr) *delegateQueuePtr = dqPtr; + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + delegate = newDelegate; + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; +} + +- (BOOL)isIPv4Enabled +{ + // Note: YES means kIPv4Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv4Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kIPv4Disabled) == 0); + }); + + return result; + } +} + +- (void)setIPv4Enabled:(BOOL)flag +{ + // Note: YES means kIPv4Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kIPv4Disabled; + else + config |= kIPv4Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv6Enabled +{ + // Note: YES means kIPv6Disabled is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kIPv6Disabled) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kIPv6Disabled) == 0); + }); + + return result; + } +} + +- (void)setIPv6Enabled:(BOOL)flag +{ + // Note: YES means kIPv6Disabled is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kIPv6Disabled; + else + config |= kIPv6Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv4PreferredOverIPv6 +{ + // Note: YES means kPreferIPv6 is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kPreferIPv6) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kPreferIPv6) == 0); + }); + + return result; + } +} + +- (void)setIPv4PreferredOverIPv6:(BOOL)flag +{ + // Note: YES means kPreferIPv6 is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kPreferIPv6; + else + config |= kPreferIPv6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (id)userData +{ + __block id result = nil; + + dispatch_block_t block = ^{ + + result = userData; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setUserData:(id)arbitraryUserData +{ + dispatch_block_t block = ^{ + + if (userData != arbitraryUserData) + { + userData = arbitraryUserData; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Accepting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self acceptOnInterface:nil port:port error:errPtr]; +} + +- (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr +{ + LogTrace(); + + // Just in-case interface parameter is immutable. + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + // CreateSocket Block + // This block will be invoked within the dispatch block below. + + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { + + int socketFD = socket(domain, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + NSString *reason = @"Error in socket() function"; + err = [self errnoErrorWithReason:reason]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + int reuseOn = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + if (status == -1) + { + NSString *reason = @"Error enabling address reuse (setsockopt)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Bind socket + + status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); + if (status == -1) + { + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Listen + + status = listen(socketFD, 1024); + if (status == -1) + { + NSString *reason = @"Error in listen() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + return socketFD; + }; + + // Create dispatch block and run on socketQueue + + dispatch_block_t block = ^{ @autoreleasepool { + + if (delegate == nil) // Must have delegate set + { + NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (![self isDisconnected]) // Must be disconnected + { + NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + // Resolve interface from description + + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port]; + + if ((interface4 == nil) && (interface6 == nil)) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL enableIPv4 = !isIPv4Disabled && (interface4 != nil); + BOOL enableIPv6 = !isIPv6Disabled && (interface6 != nil); + + // Create sockets, configure, bind, and listen + + if (enableIPv4) + { + LogVerbose(@"Creating IPv4 socket"); + socket4FD = createSocket(AF_INET, interface4); + + if (socket4FD == SOCKET_NULL) + { + return_from_block; + } + } + + if (enableIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + if (enableIPv4 && (port == 0)) + { + // No specific port was specified, so we allowed the OS to pick an available port for us. + // Now we need to make sure the IPv6 socket listens on the same port as the IPv4 socket. + + struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)[interface6 mutableBytes]; + addr6->sin6_port = htons([self localPort4]); + } + + socket6FD = createSocket(AF_INET6, interface6); + + if (socket6FD == SOCKET_NULL) + { + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(socket4FD); + } + + return_from_block; + } + } + + // Create accept sources + + if (enableIPv4) + { + accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue); + + int socketFD = socket4FD; + dispatch_source_t acceptSource = accept4Source; + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(accept4Source, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"event4Block"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); + + #pragma clang diagnostic pop + }}); + + + dispatch_source_set_cancel_handler(accept4Source, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(accept4Source)"); + dispatch_release(acceptSource); + #endif + + LogVerbose(@"close(socket4FD)"); + close(socketFD); + + #pragma clang diagnostic pop + }); + + LogVerbose(@"dispatch_resume(accept4Source)"); + dispatch_resume(accept4Source); + } + + if (enableIPv6) + { + accept6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue); + + int socketFD = socket6FD; + dispatch_source_t acceptSource = accept6Source; + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(accept6Source, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"event6Block"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); + + #pragma clang diagnostic pop + }}); + + dispatch_source_set_cancel_handler(accept6Source, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(accept6Source)"); + dispatch_release(acceptSource); + #endif + + LogVerbose(@"close(socket6FD)"); + close(socketFD); + + #pragma clang diagnostic pop + }); + + LogVerbose(@"dispatch_resume(accept6Source)"); + dispatch_resume(accept6Source); + } + + flags |= kSocketStarted; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + LogInfo(@"Error in accept: %@", err); + + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr; +{ + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + // CreateSocket Block + // This block will be invoked within the dispatch block below. + + int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { + + int socketFD = socket(domain, SOCK_STREAM, 0); + + if (socketFD == SOCKET_NULL) + { + NSString *reason = @"Error in socket() function"; + err = [self errnoErrorWithReason:reason]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + int reuseOn = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + if (status == -1) + { + NSString *reason = @"Error enabling address reuse (setsockopt)"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Bind socket + + status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); + if (status == -1) + { + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + // Listen + + status = listen(socketFD, 1024); + if (status == -1) + { + NSString *reason = @"Error in listen() function"; + err = [self errnoErrorWithReason:reason]; + + LogVerbose(@"close(socketFD)"); + close(socketFD); + return SOCKET_NULL; + } + + return socketFD; + }; + + // Create dispatch block and run on socketQueue + + dispatch_block_t block = ^{ @autoreleasepool { + + if (delegate == nil) // Must have delegate set + { + NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + if (![self isDisconnected]) // Must be disconnected + { + NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; + err = [self badConfigError:msg]; + + return_from_block; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + // Remove a previous socket + + NSError *error = nil; + NSFileManager *fileManager = [NSFileManager defaultManager]; + if ([fileManager fileExistsAtPath:url.path]) { + if (![[NSFileManager defaultManager] removeItemAtURL:url error:&error]) { + NSString *msg = @"Could not remove previous unix domain socket at given url."; + err = [self otherError:msg]; + + return_from_block; + } + } + + // Resolve interface from description + + NSData *interface = [self getInterfaceAddressFromUrl:url]; + + if (interface == nil) + { + NSString *msg = @"Invalid unix domain url. Specify a valid file url that does not exist (e.g. \"file:///tmp/socket\")"; + err = [self badParamError:msg]; + + return_from_block; + } + + // Create sockets, configure, bind, and listen + + LogVerbose(@"Creating unix domain socket"); + socketUN = createSocket(AF_UNIX, interface); + + if (socketUN == SOCKET_NULL) + { + return_from_block; + } + + socketUrl = url; + + // Create accept sources + + acceptUNSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketUN, 0, socketQueue); + + int socketFD = socketUN; + dispatch_source_t acceptSource = acceptUNSource; + + dispatch_source_set_event_handler(acceptUNSource, ^{ @autoreleasepool { + + LogVerbose(@"eventUNBlock"); + + unsigned long i = 0; + unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); + + LogVerbose(@"numPendingConnections: %lu", numPendingConnections); + + while ([self doAccept:socketFD] && (++i < numPendingConnections)); + }}); + + dispatch_source_set_cancel_handler(acceptUNSource, ^{ + +#if NEEDS_DISPATCH_RETAIN_RELEASE + LogVerbose(@"dispatch_release(accept4Source)"); + dispatch_release(acceptSource); +#endif + + LogVerbose(@"close(socket4FD)"); + close(socketFD); + }); + + LogVerbose(@"dispatch_resume(accept4Source)"); + dispatch_resume(acceptUNSource); + + flags |= kSocketStarted; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + LogInfo(@"Error in accept: %@", err); + + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)doAccept:(int)parentSocketFD +{ + LogTrace(); + + int socketType; + int childSocketFD; + NSData *childSocketAddress; + + if (parentSocketFD == socket4FD) + { + socketType = 0; + + struct sockaddr_in addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + else if (parentSocketFD == socket6FD) + { + socketType = 1; + + struct sockaddr_in6 addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + else // if (parentSocketFD == socketUN) + { + socketType = 2; + + struct sockaddr_un addr; + socklen_t addrLen = sizeof(addr); + + childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); + + if (childSocketFD == -1) + { + LogWarn(@"Accept failed with error: %@", [self errnoError]); + return NO; + } + + childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; + } + + // Enable non-blocking IO on the socket + + int result = fcntl(childSocketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + LogWarn(@"Error enabling non-blocking IO on accepted socket (fcntl)"); + return NO; + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Notify delegate + + if (delegateQueue) + { + __strong id theDelegate = delegate; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + // Query delegate for custom socket queue + + dispatch_queue_t childSocketQueue = NULL; + + if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)]) + { + childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress + onSocket:self]; + } + + // Create GCDAsyncSocket instance for accepted socket + + GCDAsyncSocket *acceptedSocket = [[[self class] alloc] initWithDelegate:theDelegate + delegateQueue:delegateQueue + socketQueue:childSocketQueue]; + + if (socketType == 0) + acceptedSocket->socket4FD = childSocketFD; + else if (socketType == 1) + acceptedSocket->socket6FD = childSocketFD; + else + acceptedSocket->socketUN = childSocketFD; + + acceptedSocket->flags = (kSocketStarted | kConnected); + + // Setup read and write sources for accepted socket + + dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool { + + [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD]; + }}); + + // Notify delegate + + if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) + { + [theDelegate socket:self didAcceptNewSocket:acceptedSocket]; + } + + // Release the socket queue returned from the delegate (it was retained by acceptedSocket) + #if !OS_OBJECT_USE_OBJC + if (childSocketQueue) dispatch_release(childSocketQueue); + #endif + + // The accepted socket should have been retained by the delegate. + // Otherwise it gets properly released when exiting the block. + }}); + } + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method runs through the various checks required prior to a connection attempt. + * It is shared between the connectToHost and connectToAddress methods. + * +**/ +- (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (![self isDisconnected]) // Must be disconnected + { + if (errPtr) + { + NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + if (errPtr) + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (interface) + { + NSMutableData *interface4 = nil; + NSMutableData *interface6 = nil; + + [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0]; + + if ((interface4 == nil) && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv4Disabled && (interface6 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + if (errPtr) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + connectInterface4 = interface4; + connectInterface6 = interface6; + } + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + return YES; +} + +- (BOOL)preConnectWithUrl:(NSURL *)url error:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (![self isDisconnected]) // Must be disconnected + { + if (errPtr) + { + NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + NSData *interface = [self getInterfaceAddressFromUrl:url]; + + if (interface == nil) + { + if (errPtr) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + *errPtr = [self badParamError:msg]; + } + return NO; + } + + connectInterfaceUN = interface; + + // Clear queues (spurious read/write requests post disconnect) + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + return YES; +} + +- (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToHost:(NSString *)host + onPort:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToHost:(NSString *)inHost + onPort:(uint16_t)port + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSString *host = [inHost copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *preConnectErr = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with host parameter + + if ([host length] == 0) + { + NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; + preConnectErr = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&preConnectErr]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + flags |= kSocketStarted; + + LogVerbose(@"Dispatching DNS lookup..."); + + // It's possible that the given host parameter is actually a NSMutableString. + // So we want to copy it now, within this block that will be executed synchronously. + // This way the asynchronous lookup block below doesn't have to worry about it changing. + + NSString *hostCpy = [host copy]; + + int aStateIndex = stateIndex; + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + NSError *lookupErr = nil; + NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr]; + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + if (lookupErr) + { + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf lookup:aStateIndex didFail:lookupErr]; + }}); + } + else + { + NSData *address4 = nil; + NSData *address6 = nil; + + for (NSData *address in addresses) + { + if (!address4 && [[self class] isIPv4Address:address]) + { + address4 = address; + } + else if (!address6 && [[self class] isIPv6Address:address]) + { + address6 = address; + } + } + + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; + }}); + } + + #pragma clang diagnostic pop + }}); + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + + if (errPtr) *errPtr = preConnectErr; + return result; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:timeout error:errPtr]; +} + +- (BOOL)connectToAddress:(NSData *)inRemoteAddr + viaInterface:(NSString *)inInterface + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + LogTrace(); + + // Just in case immutable objects were passed + NSData *remoteAddr = [inRemoteAddr copy]; + NSString *interface = [inInterface copy]; + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with remoteAddr parameter + + NSData *address4 = nil; + NSData *address6 = nil; + + if ([remoteAddr length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddr = (const struct sockaddr *)[remoteAddr bytes]; + + if (sockaddr->sa_family == AF_INET) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in)) + { + address4 = remoteAddr; + } + } + else if (sockaddr->sa_family == AF_INET6) + { + if ([remoteAddr length] == sizeof(struct sockaddr_in6)) + { + address6 = remoteAddr; + } + } + } + + if ((address4 == nil) && (address6 == nil)) + { + NSString *msg = @"A valid IPv4 or IPv6 address was not given"; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address4 != nil)) + { + NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (address6 != nil)) + { + NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithInterface:interface error:&err]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + return_from_block; + } + + flags |= kSocketStarted; + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; +{ + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Check for problems with host parameter + + if ([url.path length] == 0) + { + NSString *msg = @"Invalid unix domain socket url."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Run through standard pre-connect checks + + if (![self preConnectWithUrl:url error:&err]) + { + return_from_block; + } + + // We've made it past all the checks. + // It's time to start the connection process. + + flags |= kSocketStarted; + + // Start the normal connection process + + NSError *connectError = nil; + if (![self connectWithAddressUN:connectInterfaceUN error:&connectError]) + { + [self closeWithError:connectError]; + + return_from_block; + } + + [self startConnectTimeout:timeout]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (result == NO) + { + if (errPtr) + *errPtr = err; + } + + return result; +} + +- (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(address4 || address6, @"Expected at least one valid address"); + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + // Check for problems + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (address6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + if (isIPv6Disabled && (address4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + // Start the normal connection process + + NSError *err = nil; + if (![self connectWithAddress4:address4 address6:address6 error:&err]) + { + [self closeWithError:err]; + } +} + +/** + * This method is called if the DNS lookup fails. + * This method is executed on the socketQueue. + * + * Since the DNS lookup executed synchronously on a global concurrent queue, + * the original connection request may have already been cancelled or timed-out by the time this method is invoked. + * The lookupIndex tells us whether the lookup is still valid or not. +**/ +- (void)lookup:(int)aStateIndex didFail:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring lookup:didFail: - already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self endConnectTimeout]; + [self closeWithError:error]; +} + +- (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]); + LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]); + + // Determine socket type + + BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; + + BOOL useIPv6 = ((preferIPv6 && address6) || (address4 == nil)); + + // Create the socket + + int socketFD; + NSData *address; + NSData *connectInterface; + + if (useIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + socket6FD = socket(AF_INET6, SOCK_STREAM, 0); + + socketFD = socket6FD; + address = address6; + connectInterface = connectInterface6; + } + else + { + LogVerbose(@"Creating IPv4 socket"); + + socket4FD = socket(AF_INET, SOCK_STREAM, 0); + + socketFD = socket4FD; + address = address4; + connectInterface = connectInterface4; + } + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; + + return NO; + } + + // Bind the socket to the desired interface (if needed) + + if (connectInterface) + { + LogVerbose(@"Binding socket..."); + + if ([[self class] portFromAddress:connectInterface] > 0) + { + // Since we're going to be binding to a specific port, + // we should turn on reuseaddr to allow us to override sockets in time_wait. + + int reuseOn = 1; + setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + } + + const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes]; + + int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]); + if (result != 0) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; + + return NO; + } + } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Start the connection process in a background queue + + int aStateIndex = stateIndex; + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]); + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + if (result == 0) + { + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf didConnect:aStateIndex]; + }}); + } + else + { + NSError *error = [strongSelf errnoErrorWithReason:@"Error in connect() function"]; + + dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { + + [strongSelf didNotConnect:aStateIndex error:error]; + }}); + } + + #pragma clang diagnostic pop + }); + + LogVerbose(@"Connecting..."); + + return YES; +} + +- (BOOL)connectWithAddressUN:(NSData *)address error:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + // Create the socket + + int socketFD; + + LogVerbose(@"Creating unix domain socket"); + + socketUN = socket(AF_UNIX, SOCK_STREAM, 0); + + socketFD = socketUN; + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; + + return NO; + } + + // Bind the socket to the desired interface (if needed) + + LogVerbose(@"Binding socket..."); + + int reuseOn = 1; + setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + +// const struct sockaddr *interfaceAddr = (const struct sockaddr *)[address bytes]; +// +// int result = bind(socketFD, interfaceAddr, (socklen_t)[address length]); +// if (result != 0) +// { +// if (errPtr) +// *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; +// +// return NO; +// } + + // Prevent SIGPIPE signals + + int nosigpipe = 1; + setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + + // Start the connection process in a background queue + + int aStateIndex = stateIndex; + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ + + const struct sockaddr *addr = (const struct sockaddr *)[address bytes]; + int result = connect(socketFD, addr, addr->sa_len); + if (result == 0) + { + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self didConnect:aStateIndex]; + }}); + } + else + { + // TODO: Bad file descriptor + perror("connect"); + NSError *error = [self errnoErrorWithReason:@"Error in connect() function"]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self didNotConnect:aStateIndex error:error]; + }}); + } + }); + + LogVerbose(@"Connecting..."); + + return YES; +} + +- (void)didConnect:(int)aStateIndex +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring didConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + flags |= kConnected; + + [self endConnectTimeout]; + + #if TARGET_OS_IPHONE + // The endConnectTimeout method executed above incremented the stateIndex. + aStateIndex = stateIndex; + #endif + + // Setup read/write streams (as workaround for specific shortcomings in the iOS platform) + // + // Note: + // There may be configuration options that must be set by the delegate before opening the streams. + // The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream. + // + // Thus we wait until after the socket:didConnectToHost:port: delegate method has completed. + // This gives the delegate time to properly configure the streams if needed. + + dispatch_block_t SetupStreamsPart1 = ^{ + #if TARGET_OS_IPHONE + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:NO]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + + #endif + }; + dispatch_block_t SetupStreamsPart2 = ^{ + #if TARGET_OS_IPHONE + + if (aStateIndex != stateIndex) + { + // The socket has been disconnected. + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error creating CFStreams"]]; + return; + } + + #endif + }; + + // Notify delegate + + NSString *host = [self connectedHost]; + uint16_t port = [self connectedPort]; + NSURL *url = [self connectedUrl]; + + __strong id theDelegate = delegate; + + if (delegateQueue && host != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) + { + SetupStreamsPart1(); + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didConnectToHost:host port:port]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + SetupStreamsPart2(); + }}); + }}); + } + else if (delegateQueue && url != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToUrl:)]) + { + SetupStreamsPart1(); + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didConnectToUrl:url]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + SetupStreamsPart2(); + }}); + }}); + } + else + { + SetupStreamsPart1(); + SetupStreamsPart2(); + } + + // Get the connected socket + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + // Enable non-blocking IO on the socket + + int result = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (result == -1) + { + NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)"; + [self closeWithError:[self otherError:errMsg]]; + + return; + } + + // Setup our read/write sources + + [self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD]; + + // Dequeue any pending read/write requests + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; +} + +- (void)didNotConnect:(int)aStateIndex error:(NSError *)error +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring didNotConnect, already disconnected"); + + // The connect operation has been cancelled. + // That is, socket was disconnected, or connection has already timed out. + return; + } + + [self closeWithError:error]; +} + +- (void)startConnectTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doConnectTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theConnectTimer = connectTimer; + dispatch_source_set_cancel_handler(connectTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(connectTimer)"); + dispatch_release(theConnectTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0); + + dispatch_resume(connectTimer); + } +} + +- (void)endConnectTimeout +{ + LogTrace(); + + if (connectTimer) + { + dispatch_source_cancel(connectTimer); + connectTimer = NULL; + } + + // Increment stateIndex. + // This will prevent us from processing results from any related background asynchronous operations. + // + // Note: This should be called from close method even if connectTimer is NULL. + // This is because one might disconnect a socket prior to a successful connection which had no timeout. + + stateIndex++; + + if (connectInterface4) + { + connectInterface4 = nil; + } + if (connectInterface6) + { + connectInterface6 = nil; + } +} + +- (void)doConnectTimeout +{ + LogTrace(); + + [self endConnectTimeout]; + [self closeWithError:[self connectTimeoutError]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Disconnecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)closeWithError:(NSError *)error +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + [self endConnectTimeout]; + + if (currentRead != nil) [self endCurrentRead]; + if (currentWrite != nil) [self endCurrentWrite]; + + [readQueue removeAllObjects]; + [writeQueue removeAllObjects]; + + [preBuffer reset]; + + #if TARGET_OS_IPHONE + { + if (readStream || writeStream) + { + [self removeStreamsFromRunLoop]; + + if (readStream) + { + CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL); + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL); + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + } + } + #endif + + [sslPreBuffer reset]; + sslErrCode = lastSSLHandshakeError = noErr; + + if (sslContext) + { + // Getting a linker error here about the SSLx() functions? + // You need to add the Security Framework to your application. + + SSLClose(sslContext); + + #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) + CFRelease(sslContext); + #else + SSLDisposeContext(sslContext); + #endif + + sslContext = NULL; + } + + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + // So we have to unpause the source if needed. + // This allows the cancel handler to be run, which in turn releases the source and closes the socket. + + if (!accept4Source && !accept6Source && !acceptUNSource && !readSource && !writeSource) + { + LogVerbose(@"manually closing close"); + + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"close(socket4FD)"); + close(socket4FD); + socket4FD = SOCKET_NULL; + } + + if (socket6FD != SOCKET_NULL) + { + LogVerbose(@"close(socket6FD)"); + close(socket6FD); + socket6FD = SOCKET_NULL; + } + + if (socketUN != SOCKET_NULL) + { + LogVerbose(@"close(socketUN)"); + close(socketUN); + socketUN = SOCKET_NULL; + unlink(socketUrl.path.fileSystemRepresentation); + socketUrl = nil; + } + } + else + { + if (accept4Source) + { + LogVerbose(@"dispatch_source_cancel(accept4Source)"); + dispatch_source_cancel(accept4Source); + + // We never suspend accept4Source + + accept4Source = NULL; + } + + if (accept6Source) + { + LogVerbose(@"dispatch_source_cancel(accept6Source)"); + dispatch_source_cancel(accept6Source); + + // We never suspend accept6Source + + accept6Source = NULL; + } + + if (acceptUNSource) + { + LogVerbose(@"dispatch_source_cancel(acceptUNSource)"); + dispatch_source_cancel(acceptUNSource); + + // We never suspend acceptUNSource + + acceptUNSource = NULL; + } + + if (readSource) + { + LogVerbose(@"dispatch_source_cancel(readSource)"); + dispatch_source_cancel(readSource); + + [self resumeReadSource]; + + readSource = NULL; + } + + if (writeSource) + { + LogVerbose(@"dispatch_source_cancel(writeSource)"); + dispatch_source_cancel(writeSource); + + [self resumeWriteSource]; + + writeSource = NULL; + } + + // The sockets will be closed by the cancel handlers of the corresponding source + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + socketUN = SOCKET_NULL; + } + + // If the client has passed the connect/accept method, then the connection has at least begun. + // Notify delegate that it is now ending. + BOOL shouldCallDelegate = (flags & kSocketStarted) ? YES : NO; + BOOL isDeallocating = (flags & kDealloc) ? YES : NO; + + // Clear stored socket info and all flags (config remains as is) + socketFDBytesAvailable = 0; + flags = 0; + sslWriteCachedLength = 0; + + if (shouldCallDelegate) + { + __strong id theDelegate = delegate; + __strong id theSelf = isDeallocating ? nil : self; + + if (delegateQueue && [theDelegate respondsToSelector: @selector(socketDidDisconnect:withError:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidDisconnect:theSelf withError:error]; + }}); + } + } +} + +- (void)disconnect +{ + dispatch_block_t block = ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + [self closeWithError:nil]; + } + }}; + + // Synchronous disconnection, as documented in the header file + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +- (void)disconnectAfterReading +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterReads); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +- (void)disconnectAfterReadingAndWriting +{ + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if (flags & kSocketStarted) + { + flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); + [self maybeClose]; + } + }}); +} + +/** + * Closes the socket if possible. + * That is, if all writes have completed, and we're set to disconnect after writing, + * or if all reads have completed, and we're set to disconnect after reading. +**/ +- (void)maybeClose +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + BOOL shouldClose = NO; + + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + else + { + shouldClose = YES; + } + } + } + else if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + shouldClose = YES; + } + } + + if (shouldClose) + { + [self closeWithError:nil]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSError *)badConfigError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadConfigError userInfo:userInfo]; +} + +- (NSError *)badParamError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; +} + ++ (NSError *)gaiError:(int)gai_error +{ + NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; +} + +- (NSError *)errnoErrorWithReason:(NSString *)reason +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:errMsg, NSLocalizedDescriptionKey, + reason, NSLocalizedFailureReasonErrorKey, nil]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)errnoError +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)sslError:(OSStatus)ssl_error +{ + NSString *msg = @"Error code definition can be found in Apple's SecureTransport.h"; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:msg forKey:NSLocalizedRecoverySuggestionErrorKey]; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainSSL" code:ssl_error userInfo:userInfo]; +} + +- (NSError *)connectTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketConnectTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Attempt to connect to host timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketConnectTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket maxed out error. +**/ +- (NSError *)readMaxedOutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadMaxedOutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Read operation reached set maximum length", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadMaxedOutError userInfo:info]; +} + +/** + * Returns a standard AsyncSocket write timeout error. +**/ +- (NSError *)readTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Read operation timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadTimeoutError userInfo:userInfo]; +} + +/** + * Returns a standard AsyncSocket write timeout error. +**/ +- (NSError *)writeTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketWriteTimeoutError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Write operation timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketWriteTimeoutError userInfo:userInfo]; +} + +- (NSError *)connectionClosedError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketClosedError", + @"GCDAsyncSocket", [NSBundle mainBundle], + @"Socket closed by remote peer", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketClosedError userInfo:userInfo]; +} + +- (NSError *)otherError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Diagnostics +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isDisconnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kSocketStarted) ? NO : YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isConnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kConnected) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSString *)connectedHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socket4FD != SOCKET_NULL) + result = [self connectedHostFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self connectedHostFromSocket6:socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)connectedPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (socket4FD != SOCKET_NULL) + result = [self connectedPortFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self connectedPortFromSocket6:socket6FD]; + }); + + return result; + } +} + +- (NSURL *)connectedUrl +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socketUN != SOCKET_NULL) + return [self connectedUrlFromSocketUN:socketUN]; + + return nil; + } + else + { + __block NSURL *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socketUN != SOCKET_NULL) + result = [self connectedUrlFromSocketUN:socketUN]; + }}); + + return result; + } +} + +- (NSString *)localHost +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; + } + else + { + __block NSString *result = nil; + + dispatch_sync(socketQueue, ^{ @autoreleasepool { + + if (socket4FD != SOCKET_NULL) + result = [self localHostFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self localHostFromSocket6:socket6FD]; + }}); + + return result; + } +} + +- (uint16_t)localPort +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; + } + else + { + __block uint16_t result = 0; + + dispatch_sync(socketQueue, ^{ + // No need for autorelease pool + + if (socket4FD != SOCKET_NULL) + result = [self localPortFromSocket4:socket4FD]; + else if (socket6FD != SOCKET_NULL) + result = [self localPortFromSocket6:socket6FD]; + }); + + return result; + } +} + +- (NSString *)connectedHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)connectedHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)connectedPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self connectedPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)connectedPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self connectedPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)localHost4 +{ + if (socket4FD != SOCKET_NULL) + return [self localHostFromSocket4:socket4FD]; + + return nil; +} + +- (NSString *)localHost6 +{ + if (socket6FD != SOCKET_NULL) + return [self localHostFromSocket6:socket6FD]; + + return nil; +} + +- (uint16_t)localPort4 +{ + if (socket4FD != SOCKET_NULL) + return [self localPortFromSocket4:socket4FD]; + + return 0; +} + +- (uint16_t)localPort6 +{ + if (socket6FD != SOCKET_NULL) + return [self localPortFromSocket6:socket6FD]; + + return 0; +} + +- (NSString *)connectedHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)connectedHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)connectedPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)connectedPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSURL *)connectedUrlFromSocketUN:(int)socketFD +{ + struct sockaddr_un sockaddr; + socklen_t sockaddrlen = sizeof(sockaddr); + + if (getpeername(socketFD, (struct sockaddr *)&sockaddr, &sockaddrlen) < 0) + { + return 0; + } + return [[self class] urlFromSockaddrUN:&sockaddr]; +} + +- (NSString *)localHostFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr4:&sockaddr4]; +} + +- (NSString *)localHostFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [[self class] hostFromSockaddr6:&sockaddr6]; +} + +- (uint16_t)localPortFromSocket4:(int)socketFD +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr4:&sockaddr4]; +} + +- (uint16_t)localPortFromSocket6:(int)socketFD +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [[self class] portFromSockaddr6:&sockaddr6]; +} + +- (NSData *)connectedAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (NSData *)localAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; + } + } + + if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; + } + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPv4 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket4FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (socket4FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isIPv6 +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (socket6FD != SOCKET_NULL); + } + else + { + __block BOOL result = NO; + + dispatch_sync(socketQueue, ^{ + result = (socket6FD != SOCKET_NULL); + }); + + return result; + } +} + +- (BOOL)isSecure +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return (flags & kSocketSecure) ? YES : NO; + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = (flags & kSocketSecure) ? YES : NO; + }); + + return result; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Finds the address of an interface description. + * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). + * + * The interface description may optionally contain a port number at the end, separated by a colon. + * If a non-zero port parameter is provided, any port number in the interface description is ignored. + * + * The returned value is a 'struct sockaddr' wrapped in an NSMutableData object. +**/ +- (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr + address6:(NSMutableData **)interfaceAddr6Ptr + fromDescription:(NSString *)interfaceDescription + port:(uint16_t)port +{ + NSMutableData *addr4 = nil; + NSMutableData *addr6 = nil; + + NSString *interface = nil; + + NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; + if ([components count] > 0) + { + NSString *temp = [components objectAtIndex:0]; + if ([temp length] > 0) + { + interface = temp; + } + } + if ([components count] > 1 && port == 0) + { + long portL = strtol([[components objectAtIndex:1] UTF8String], NULL, 10); + + if (portL > 0 && portL <= UINT16_MAX) + { + port = (uint16_t)portL; + } + } + + if (interface == nil) + { + // ANY address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_any; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) + { + // LOOPBACK address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_loopback; + + addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else + { + const char *iface = [interface UTF8String]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) + { + // IPv4 + + struct sockaddr_in nativeAddr4; + memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + else + { + char ip[INET_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr4.sin_port = htons(port); + + addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + } + } + else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) + { + // IPv6 + + struct sockaddr_in6 nativeAddr6; + memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else + { + char ip[INET6_ADDRSTRLEN]; + + const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + } + + if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; + if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; +} + +- (NSData *)getInterfaceAddressFromUrl:(NSURL *)url; +{ + NSString *path = url.path; + if (path.length == 0) { + return nil; + } + + struct sockaddr_un nativeAddr; + nativeAddr.sun_family = AF_UNIX; + strlcpy(nativeAddr.sun_path, path.fileSystemRepresentation, sizeof(nativeAddr.sun_path)); + nativeAddr.sun_len = SUN_LEN(&nativeAddr); + NSData *interface = [NSData dataWithBytes:&nativeAddr length:sizeof(struct sockaddr_un)]; + + return interface; +} + +- (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD +{ + readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue); + writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue); + + // Setup event handlers + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"readEventBlock"); + + strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource); + LogVerbose(@"socketFDBytesAvailable: %lu", strongSelf->socketFDBytesAvailable); + + if (strongSelf->socketFDBytesAvailable > 0) + [strongSelf doReadData]; + else + [strongSelf doReadEOF]; + + #pragma clang diagnostic pop + }}); + + dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + LogVerbose(@"writeEventBlock"); + + strongSelf->flags |= kSocketCanAcceptBytes; + [strongSelf doWriteData]; + + #pragma clang diagnostic pop + }}); + + // Setup cancel handlers + + __block int socketFDRefCount = 2; + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadSource = readSource; + dispatch_source_t theWriteSource = writeSource; + #endif + + dispatch_source_set_cancel_handler(readSource, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"readCancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(readSource)"); + dispatch_release(theReadSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + + #pragma clang diagnostic pop + }); + + dispatch_source_set_cancel_handler(writeSource, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"writeCancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(writeSource)"); + dispatch_release(theWriteSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socketFD)"); + close(socketFD); + } + + #pragma clang diagnostic pop + }); + + // We will not be able to read until data arrives. + // But we should be able to write immediately. + + socketFDBytesAvailable = 0; + flags &= ~kReadSourceSuspended; + + LogVerbose(@"dispatch_resume(readSource)"); + dispatch_resume(readSource); + + flags |= kSocketCanAcceptBytes; + flags |= kWriteSourceSuspended; +} + +- (BOOL)usingCFStreamForTLS +{ + #if TARGET_OS_IPHONE + + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. + + return YES; + } + + #endif + + return NO; +} + +- (BOOL)usingSecureTransportForTLS +{ + // Invoking this method is equivalent to ![self usingCFStreamForTLS] (just more readable) + + #if TARGET_OS_IPHONE + + if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) + { + // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. + + return NO; + } + + #endif + + return YES; +} + +- (void)suspendReadSource +{ + if (!(flags & kReadSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(readSource)"); + + dispatch_suspend(readSource); + flags |= kReadSourceSuspended; + } +} + +- (void)resumeReadSource +{ + if (flags & kReadSourceSuspended) + { + LogVerbose(@"dispatch_resume(readSource)"); + + dispatch_resume(readSource); + flags &= ~kReadSourceSuspended; + } +} + +- (void)suspendWriteSource +{ + if (!(flags & kWriteSourceSuspended)) + { + LogVerbose(@"dispatch_suspend(writeSource)"); + + dispatch_suspend(writeSource); + flags |= kWriteSourceSuspended; + } +} + +- (void)resumeWriteSource +{ + if (flags & kWriteSourceSuspended) + { + LogVerbose(@"dispatch_resume(writeSource)"); + + dispatch_resume(writeSource); + flags &= ~kWriteSourceSuspended; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Reading +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag +{ + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:length + timeout:timeout + readLength:0 + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag]; +} + +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + if (length == 0) { + LogWarn(@"Cannot read: length == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:0 + timeout:timeout + readLength:length + terminator:nil + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:length tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)maxLength + tag:(long)tag +{ + if ([data length] == 0) { + LogWarn(@"Cannot read: [data length] == 0"); + return; + } + if (offset > [buffer length]) { + LogWarn(@"Cannot read: offset > [buffer length]"); + return; + } + if (maxLength > 0 && maxLength < [data length]) { + LogWarn(@"Cannot read: maxLength > 0 && maxLength < [data length]"); + return; + } + + GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:maxLength + timeout:timeout + readLength:0 + terminator:data + tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [self maybeDequeueRead]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!currentRead || ![currentRead isKindOfClass:[GCDAsyncReadPacket class]]) + { + // We're not reading anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + // It's only possible to know the progress of our read if we're reading to a certain length. + // If we're reading to data, we of course have no idea when the data will arrive. + // If we're reading to timeout, then we have no idea when the next chunk of data will arrive. + + NSUInteger done = currentRead->bytesDone; + NSUInteger total = currentRead->readLength; + + if (tagPtr != NULL) *tagPtr = currentRead->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + if (total > 0) + result = (float)done / (float)total; + else + result = 1.0F; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * This method starts a new read, if needed. + * + * It is called when: + * - a user requests a read + * - after a read request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. +**/ +- (void)maybeDequeueRead +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + // If we're not currently processing a read AND we have an available read stream + if ((currentRead == nil) && (flags & kConnected)) + { + if ([readQueue count] > 0) + { + // Dequeue the next object in the write queue + currentRead = [readQueue objectAtIndex:0]; + [readQueue removeObjectAtIndex:0]; + + + if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingReadTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncReadPacket"); + + // Setup read timer (if needed) + [self setupReadTimerWithTimeout:currentRead->timeout]; + + // Immediately read, if possible + [self doReadData]; + } + } + else if (flags & kDisconnectAfterReads) + { + if (flags & kDisconnectAfterWrites) + { + if (([writeQueue count] == 0) && (currentWrite == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + else if (flags & kSocketSecure) + { + [self flushSSLBuffers]; + + // Edge case: + // + // We just drained all data from the ssl buffers, + // and all known data from the socket (socketFDBytesAvailable). + // + // If we didn't get any data from this process, + // then we may have reached the end of the TCP stream. + // + // Be sure callbacks are enabled so we're notified about a disconnection. + + if ([preBuffer availableBytes] == 0) + { + if ([self usingCFStreamForTLS]) { + // Callbacks never disabled + } + else { + [self resumeReadSource]; + } + } + } + } +} + +- (void)flushSSLBuffers +{ + LogTrace(); + + NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket"); + + if ([preBuffer availableBytes] > 0) + { + // Only flush the ssl buffers if the prebuffer is empty. + // This is to avoid growing the prebuffer inifinitely large. + + return; + } + + #if TARGET_OS_IPHONE + + if ([self usingCFStreamForTLS]) + { + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + CFIndex defaultBytesToRead = (1024 * 4); + + [preBuffer ensureCapacityForWrite:defaultBytesToRead]; + + uint8_t *buffer = [preBuffer writeBuffer]; + + CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead); + LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result); + + if (result > 0) + { + [preBuffer didWrite:result]; + } + + flags &= ~kSecureSocketHasBytesAvailable; + } + + return; + } + + #endif + + __block NSUInteger estimatedBytesAvailable = 0; + + dispatch_block_t updateEstimatedBytesAvailable = ^{ + + // Figure out if there is any data available to be read + // + // socketFDBytesAvailable <- Number of encrypted bytes we haven't read from the bsd socket + // [sslPreBuffer availableBytes] <- Number of encrypted bytes we've buffered from bsd socket + // sslInternalBufSize <- Number of decrypted bytes SecureTransport has buffered + // + // We call the variable "estimated" because we don't know how many decrypted bytes we'll get + // from the encrypted bytes in the sslPreBuffer. + // However, we do know this is an upper bound on the estimation. + + estimatedBytesAvailable = socketFDBytesAvailable + [sslPreBuffer availableBytes]; + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + }; + + updateEstimatedBytesAvailable(); + + if (estimatedBytesAvailable > 0) + { + LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); + + BOOL done = NO; + do + { + LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable); + + // Make sure there's enough room in the prebuffer + + [preBuffer ensureCapacityForWrite:estimatedBytesAvailable]; + + // Read data into prebuffer + + uint8_t *buffer = [preBuffer writeBuffer]; + size_t bytesRead = 0; + + OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead); + LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead); + + if (bytesRead > 0) + { + [preBuffer didWrite:bytesRead]; + } + + LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]); + + if (result != noErr) + { + done = YES; + } + else + { + updateEstimatedBytesAvailable(); + } + + } while (!done && estimatedBytesAvailable > 0); + } +} + +- (void)doReadData +{ + LogTrace(); + + // This method is called on the socketQueue. + // It might be called directly, or via the readSource when data is available to be read. + + if ((currentRead == nil) || (flags & kReadsPaused)) + { + LogVerbose(@"No currentRead or kReadsPaused"); + + // Unable to read at this time + + if (flags & kSocketSecure) + { + // Here's the situation: + // + // We have an established secure connection. + // There may not be a currentRead, but there might be encrypted data sitting around for us. + // When the user does get around to issuing a read, that encrypted data will need to be decrypted. + // + // So why make the user wait? + // We might as well get a head start on decrypting some data now. + // + // The other reason we do this has to do with detecting a socket disconnection. + // The SSL/TLS protocol has it's own disconnection handshake. + // So when a secure socket is closed, a "goodbye" packet comes across the wire. + // We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection. + + [self flushSSLBuffers]; + } + + if ([self usingCFStreamForTLS]) + { + // CFReadStream only fires once when there is available data. + // It won't fire again until we've invoked CFReadStreamRead. + } + else + { + // If the readSource is firing, we need to pause it + // or else it will continue to fire over and over again. + // + // If the readSource is not firing, + // we want it to continue monitoring the socket. + + if (socketFDBytesAvailable > 0) + { + [self suspendReadSource]; + } + } + return; + } + + BOOL hasBytesAvailable = NO; + unsigned long estimatedBytesAvailable = 0; + + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // Requested CFStream, rather than SecureTransport, for TLS (via GCDAsyncSocketUseCFStreamForTLS) + + estimatedBytesAvailable = 0; + if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) + hasBytesAvailable = YES; + else + hasBytesAvailable = NO; + + #endif + } + else + { + estimatedBytesAvailable = socketFDBytesAvailable; + + if (flags & kSocketSecure) + { + // There are 2 buffers to be aware of here. + // + // We are using SecureTransport, a TLS/SSL security layer which sits atop TCP. + // We issue a read to the SecureTranport API, which in turn issues a read to our SSLReadFunction. + // Our SSLReadFunction then reads from the BSD socket and returns the encrypted data to SecureTransport. + // SecureTransport then decrypts the data, and finally returns the decrypted data back to us. + // + // The first buffer is one we create. + // SecureTransport often requests small amounts of data. + // This has to do with the encypted packets that are coming across the TCP stream. + // But it's non-optimal to do a bunch of small reads from the BSD socket. + // So our SSLReadFunction reads all available data from the socket (optimizing the sys call) + // and may store excess in the sslPreBuffer. + + estimatedBytesAvailable += [sslPreBuffer availableBytes]; + + // The second buffer is within SecureTransport. + // As mentioned earlier, there are encrypted packets coming across the TCP stream. + // SecureTransport needs the entire packet to decrypt it. + // But if the entire packet produces X bytes of decrypted data, + // and we only asked SecureTransport for X/2 bytes of data, + // it must store the extra X/2 bytes of decrypted data for the next read. + // + // The SSLGetBufferedReadSize function will tell us the size of this internal buffer. + // From the documentation: + // + // "This function does not block or cause any low-level read operations to occur." + + size_t sslInternalBufSize = 0; + SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); + + estimatedBytesAvailable += sslInternalBufSize; + } + + hasBytesAvailable = (estimatedBytesAvailable > 0); + } + + if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0)) + { + LogVerbose(@"No data available to read..."); + + // No data available to read. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + } + return; + } + + if (flags & kStartingReadTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The readQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingWriteTLS) + { + if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) + { + // We are in the process of a SSL Handshake. + // We were waiting for incoming data which has just arrived. + + [self ssl_continueSSLHandshake]; + } + } + else + { + // We are still waiting for the writeQueue to drain and start the SSL/TLS process. + // We now know data is available to read. + + if (![self usingCFStreamForTLS]) + { + // Suspend the read source or else it will continue to fire nonstop. + + [self suspendReadSource]; + } + } + + return; + } + + BOOL done = NO; // Completed read operation + NSError *error = nil; // Error occurred + + NSUInteger totalBytesReadForCurrentRead = 0; + + // + // STEP 1 - READ FROM PREBUFFER + // + + if ([preBuffer availableBytes] > 0) + { + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + NSUInteger bytesToCopy; + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + } + else + { + // Read type #1 or #2 + + bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]]; + } + + // Make sure we have enough room in the buffer for our read. + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into packet buffer + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(buffer, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the preBuffer + [preBuffer didRead:bytesToCopy]; + + LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]); + + // Update totals + + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + // + // We're done as soon as + // - we've read all available data (in prebuffer and socket) + // - we've read the maxLength of read packet. + + done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength)); + } + + } + + // + // STEP 2 - READ FROM SOCKET + // + + BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to read via socket (end of file) + BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more + + if (!done && !error && !socketEOF && hasBytesAvailable) + { + NSAssert(([preBuffer availableBytes] == 0), @"Invalid logic"); + + BOOL readIntoPreBuffer = NO; + uint8_t *buffer = NULL; + size_t bytesRead = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // Using CFStream, rather than SecureTransport, for TLS + + NSUInteger defaultReadLength = (1024 * 32); + + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // Read data into buffer + + CFIndex result = CFReadStreamRead(readStream, buffer, (CFIndex)bytesToRead); + LogVerbose(@"CFReadStreamRead(): result = %i", (int)result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFReadStreamCopyError(readStream); + } + else if (result == 0) + { + socketEOF = YES; + } + else + { + waiting = YES; + bytesRead = (size_t)result; + } + + // We only know how many decrypted bytes were read. + // The actual number of bytes read was likely more due to the overhead of the encryption. + // So we reset our flag, and rely on the next callback to alert us of more data. + flags &= ~kSecureSocketHasBytesAvailable; + + #endif + } + else + { + // Using SecureTransport for TLS + // + // We know: + // - how many bytes are available on the socket + // - how many encrypted bytes are sitting in the sslPreBuffer + // - how many decypted bytes are sitting in the sslContext + // + // But we do NOT know: + // - how many encypted bytes are sitting in the sslContext + // + // So we play the regular game of using an upper bound instead. + + NSUInteger defaultReadLength = (1024 * 32); + + if (defaultReadLength < estimatedBytesAvailable) { + defaultReadLength = estimatedBytesAvailable + (1024 * 16); + } + + NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength + shouldPreBuffer:&readIntoPreBuffer]; + + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // The documentation from Apple states: + // + // "a read operation might return errSSLWouldBlock, + // indicating that less data than requested was actually transferred" + // + // However, starting around 10.7, the function will sometimes return noErr, + // even if it didn't read as much data as requested. So we need to watch out for that. + + OSStatus result; + do + { + void *loop_buffer = buffer + bytesRead; + size_t loop_bytesToRead = (size_t)bytesToRead - bytesRead; + size_t loop_bytesRead = 0; + + result = SSLRead(sslContext, loop_buffer, loop_bytesToRead, &loop_bytesRead); + LogVerbose(@"read from secure socket = %u", (unsigned)loop_bytesRead); + + bytesRead += loop_bytesRead; + + } while ((result == noErr) && (bytesRead < bytesToRead)); + + + if (result != noErr) + { + if (result == errSSLWouldBlock) + waiting = YES; + else + { + if (result == errSSLClosedGraceful || result == errSSLClosedAbort) + { + // We've reached the end of the stream. + // Handle this the same way we would an EOF from the socket. + socketEOF = YES; + sslErrCode = result; + } + else + { + error = [self sslError:result]; + } + } + // It's possible that bytesRead > 0, even if the result was errSSLWouldBlock. + // This happens when the SSLRead function is able to read some data, + // but not the entire amount we requested. + + if (bytesRead <= 0) + { + bytesRead = 0; + } + } + + // Do not modify socketFDBytesAvailable. + // It will be updated via the SSLReadFunction(). + } + } + else + { + // Normal socket operation + + NSUInteger bytesToRead; + + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable + shouldPreBuffer:&readIntoPreBuffer]; + } + else + { + // Read type #1 or #2 + + bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable]; + } + + if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t (read param 3) + bytesToRead = SIZE_MAX; + } + + // Make sure we have enough room in the buffer for our read. + // + // We are either reading directly into the currentRead->buffer, + // or we're reading into the temporary preBuffer. + + if (readIntoPreBuffer) + { + [preBuffer ensureCapacityForWrite:bytesToRead]; + + buffer = [preBuffer writeBuffer]; + } + else + { + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; + + buffer = (uint8_t *)[currentRead->buffer mutableBytes] + + currentRead->startOffset + + currentRead->bytesDone; + } + + // Read data into buffer + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + ssize_t result = read(socketFD, buffer, (size_t)bytesToRead); + LogVerbose(@"read from socket = %i", (int)result); + + if (result < 0) + { + if (errno == EWOULDBLOCK) + waiting = YES; + else + error = [self errnoErrorWithReason:@"Error in read() function"]; + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + socketEOF = YES; + socketFDBytesAvailable = 0; + } + else + { + bytesRead = result; + + if (bytesRead < bytesToRead) + { + // The read returned less data than requested. + // This means socketFDBytesAvailable was a bit off due to timing, + // because we read from the socket right when the readSource event was firing. + socketFDBytesAvailable = 0; + } + else + { + if (socketFDBytesAvailable <= bytesRead) + socketFDBytesAvailable = 0; + else + socketFDBytesAvailable -= bytesRead; + } + + if (socketFDBytesAvailable == 0) + { + waiting = YES; + } + } + } + + if (bytesRead > 0) + { + // Check to see if the read operation is done + + if (currentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + // + // Note: We should never be using a prebuffer when we're reading a specific length of data. + + NSAssert(readIntoPreBuffer == NO, @"Invalid logic"); + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + + done = (currentRead->bytesDone == currentRead->readLength); + } + else if (currentRead->term != nil) + { + // Read type #3 - read up to a terminator + + if (readIntoPreBuffer) + { + // We just read a big chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + LogVerbose(@"read data into preBuffer - preBuffer.length = %zu", [preBuffer availableBytes]); + + // Search for the terminating sequence + + NSUInteger bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; + LogVerbose(@"copying %lu bytes from preBuffer", (unsigned long)bytesToCopy); + + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesToCopy); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesToCopy]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Update totals + currentRead->bytesDone += bytesToCopy; + totalBytesReadForCurrentRead += bytesToCopy; + + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method above + } + else + { + // We just read a big chunk of data directly into the packet's buffer. + // We need to move any overflow into the prebuffer. + + NSInteger overflow = [currentRead searchForTermAfterPreBuffering:bytesRead]; + + if (overflow == 0) + { + // Perfect match! + // Every byte we read stays in the read buffer, + // and the last byte we read was the last byte of the term. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = YES; + } + else if (overflow > 0) + { + // The term was found within the data that we read, + // and there are extra bytes that extend past the end of the term. + // We need to move these excess bytes out of the read packet and into the prebuffer. + + NSInteger underflow = bytesRead - overflow; + + // Copy excess data into preBuffer + + LogVerbose(@"copying %ld overflow bytes into preBuffer", (long)overflow); + [preBuffer ensureCapacityForWrite:overflow]; + + uint8_t *overflowBuffer = buffer + underflow; + memcpy([preBuffer writeBuffer], overflowBuffer, overflow); + + [preBuffer didWrite:overflow]; + LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); + + // Note: The completeCurrentRead method will trim the buffer for us. + + currentRead->bytesDone += underflow; + totalBytesReadForCurrentRead += underflow; + done = YES; + } + else + { + // The term was not found within the data that we read. + + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + done = NO; + } + } + + if (!done && currentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if (currentRead->bytesDone >= currentRead->maxLength) + { + error = [self readMaxedOutError]; + } + } + } + else + { + // Read type #1 - read all available data + + if (readIntoPreBuffer) + { + // We just read a chunk of data into the preBuffer + + [preBuffer didWrite:bytesRead]; + + // Now copy the data into the read packet. + // + // Recall that we didn't read directly into the packet's buffer to avoid + // over-allocating memory since we had no clue how much data was available to be read. + // + // Ensure there's room on the read packet's buffer + + [currentRead ensureCapacityForAdditionalDataOfLength:bytesRead]; + + // Copy bytes from prebuffer into read buffer + + uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + + currentRead->bytesDone; + + memcpy(readBuf, [preBuffer readBuffer], bytesRead); + + // Remove the copied bytes from the prebuffer + [preBuffer didRead:bytesRead]; + + // Update totals + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + else + { + currentRead->bytesDone += bytesRead; + totalBytesReadForCurrentRead += bytesRead; + } + + done = YES; + } + + } // if (bytesRead > 0) + + } // if (!done && !error && !socketEOF && hasBytesAvailable) + + + if (!done && currentRead->readLength == 0 && currentRead->term == nil) + { + // Read type #1 - read all available data + // + // We might arrive here if we read data from the prebuffer but not from the socket. + + done = (totalBytesReadForCurrentRead > 0); + } + + // Check to see if we're done, or if we've made progress + + if (done) + { + [self completeCurrentRead]; + + if (!error && (!socketEOF || [preBuffer availableBytes] > 0)) + { + [self maybeDequeueRead]; + } + } + else if (totalBytesReadForCurrentRead > 0) + { + // We're not done read type #2 or #3 yet, but we have read in some bytes + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)]) + { + long theReadTag = currentRead->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag]; + }}); + } + } + + // Check for errors + + if (error) + { + [self closeWithError:error]; + } + else if (socketEOF) + { + [self doReadEOF]; + } + else if (waiting) + { + if (![self usingCFStreamForTLS]) + { + // Monitor the socket for readability (if we're not already doing so) + [self resumeReadSource]; + } + } + + // Do not add any code here without first adding return statements in the error cases above. +} + +- (void)doReadEOF +{ + LogTrace(); + + // This method may be called more than once. + // If the EOF is read while there is still data in the preBuffer, + // then this method may be called continually after invocations of doReadData to see if it's time to disconnect. + + flags |= kSocketHasReadEOF; + + if (flags & kSocketSecure) + { + // If the SSL layer has any buffered data, flush it into the preBuffer now. + + [self flushSSLBuffers]; + } + + BOOL shouldDisconnect = NO; + NSError *error = nil; + + if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS)) + { + // We received an EOF during or prior to startTLS. + // The SSL/TLS handshake is now impossible, so this is an unrecoverable situation. + + shouldDisconnect = YES; + + if ([self usingSecureTransportForTLS]) + { + error = [self sslError:errSSLClosedAbort]; + } + } + else if (flags & kReadStreamClosed) + { + // The preBuffer has already been drained. + // The config allows half-duplex connections. + // We've previously checked the socket, and it appeared writeable. + // So we marked the read stream as closed and notified the delegate. + // + // As per the half-duplex contract, the socket will be closed when a write fails, + // or when the socket is manually closed. + + shouldDisconnect = NO; + } + else if ([preBuffer availableBytes] > 0) + { + LogVerbose(@"Socket reached EOF, but there is still data available in prebuffer"); + + // Although we won't be able to read any more data from the socket, + // there is existing data that has been prebuffered that we can read. + + shouldDisconnect = NO; + } + else if (config & kAllowHalfDuplexConnection) + { + // We just received an EOF (end of file) from the socket's read stream. + // This means the remote end of the socket (the peer we're connected to) + // has explicitly stated that it will not be sending us any more data. + // + // Query the socket to see if it is still writeable. (Perhaps the peer will continue reading data from us) + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + struct pollfd pfd[1]; + pfd[0].fd = socketFD; + pfd[0].events = POLLOUT; + pfd[0].revents = 0; + + poll(pfd, 1, 0); + + if (pfd[0].revents & POLLOUT) + { + // Socket appears to still be writeable + + shouldDisconnect = NO; + flags |= kReadStreamClosed; + + // Notify the delegate that we're going half-duplex + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidCloseReadStream:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidCloseReadStream:self]; + }}); + } + } + else + { + shouldDisconnect = YES; + } + } + else + { + shouldDisconnect = YES; + } + + + if (shouldDisconnect) + { + if (error == nil) + { + if ([self usingSecureTransportForTLS]) + { + if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful) + { + error = [self sslError:sslErrCode]; + } + else + { + error = [self connectionClosedError]; + } + } + else + { + error = [self connectionClosedError]; + } + } + [self closeWithError:error]; + } + else + { + if (![self usingCFStreamForTLS]) + { + // Suspend the read source (if needed) + + [self suspendReadSource]; + } + } +} + +- (void)completeCurrentRead +{ + LogTrace(); + + NSAssert(currentRead, @"Trying to complete current read when there is no current read."); + + + NSData *result = nil; + + if (currentRead->bufferOwner) + { + // We created the buffer on behalf of the user. + // Trim our buffer to be the proper size. + [currentRead->buffer setLength:currentRead->bytesDone]; + + result = currentRead->buffer; + } + else + { + // We did NOT create the buffer. + // The buffer is owned by the caller. + // Only trim the buffer if we had to increase its size. + + if ([currentRead->buffer length] > currentRead->originalBufferLength) + { + NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone; + NSUInteger origSize = currentRead->originalBufferLength; + + NSUInteger buffSize = MAX(readSize, origSize); + + [currentRead->buffer setLength:buffSize]; + } + + uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset; + + result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO]; + } + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadData:withTag:)]) + { + GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReadData:result withTag:theRead->tag]; + }}); + } + + [self endCurrentRead]; +} + +- (void)endCurrentRead +{ + if (readTimer) + { + dispatch_source_cancel(readTimer); + readTimer = NULL; + } + + currentRead = nil; +} + +- (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doReadTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theReadTimer = readTimer; + dispatch_source_set_cancel_handler(readTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(readTimer)"); + dispatch_release(theReadTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(readTimer); + } +} + +- (void)doReadTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kReadsPaused; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) + { + GCDAsyncReadPacket *theRead = currentRead; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag + elapsed:theRead->timeout + bytesDone:theRead->bytesDone]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self doReadTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doReadTimeoutWithExtension:0.0]; + } +} + +- (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentRead) + { + if (timeoutExtension > 0.0) + { + currentRead->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause reads, and continue + flags &= ~kReadsPaused; + [self doReadData]; + } + else + { + LogVerbose(@"ReadTimeout"); + + [self closeWithError:[self readTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Writing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + if ([data length] == 0) return; + + GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + LogTrace(); + + if ((flags & kSocketStarted) && !(flags & kForbidReadsWrites)) + { + [writeQueue addObject:packet]; + [self maybeDequeueWrite]; + } + }}); + + // Do not rely on the block being run in order to release the packet, + // as the queue might get released without the block completing. +} + +- (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr +{ + __block float result = 0.0F; + + dispatch_block_t block = ^{ + + if (!currentWrite || ![currentWrite isKindOfClass:[GCDAsyncWritePacket class]]) + { + // We're not writing anything right now. + + if (tagPtr != NULL) *tagPtr = 0; + if (donePtr != NULL) *donePtr = 0; + if (totalPtr != NULL) *totalPtr = 0; + + result = NAN; + } + else + { + NSUInteger done = currentWrite->bytesDone; + NSUInteger total = [currentWrite->buffer length]; + + if (tagPtr != NULL) *tagPtr = currentWrite->tag; + if (donePtr != NULL) *donePtr = done; + if (totalPtr != NULL) *totalPtr = total; + + result = (float)done / (float)total; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +/** + * Conditionally starts a new write. + * + * It is called when: + * - a user requests a write + * - after a write request has finished (to handle the next request) + * - immediately after the socket opens to handle any pending requests + * + * This method also handles auto-disconnect post read/write completion. +**/ +- (void)maybeDequeueWrite +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + // If we're not currently processing a write AND we have an available write stream + if ((currentWrite == nil) && (flags & kConnected)) + { + if ([writeQueue count] > 0) + { + // Dequeue the next object in the write queue + currentWrite = [writeQueue objectAtIndex:0]; + [writeQueue removeObjectAtIndex:0]; + + + if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]]) + { + LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); + + // Attempt to start TLS + flags |= kStartingWriteTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + LogVerbose(@"Dequeued GCDAsyncWritePacket"); + + // Setup write timer (if needed) + [self setupWriteTimerWithTimeout:currentWrite->timeout]; + + // Immediately write, if possible + [self doWriteData]; + } + } + else if (flags & kDisconnectAfterWrites) + { + if (flags & kDisconnectAfterReads) + { + if (([readQueue count] == 0) && (currentRead == nil)) + { + [self closeWithError:nil]; + } + } + else + { + [self closeWithError:nil]; + } + } + } +} + +- (void)doWriteData +{ + LogTrace(); + + // This method is called by the writeSource via the socketQueue + + if ((currentWrite == nil) || (flags & kWritesPaused)) + { + LogVerbose(@"No currentWrite or kWritesPaused"); + + // Unable to write at this time + + if ([self usingCFStreamForTLS]) + { + // CFWriteStream only fires once when there is available data. + // It won't fire again until we've invoked CFWriteStreamWrite. + } + else + { + // If the writeSource is firing, we need to pause it + // or else it will continue to fire over and over again. + + if (flags & kSocketCanAcceptBytes) + { + [self suspendWriteSource]; + } + } + return; + } + + if (!(flags & kSocketCanAcceptBytes)) + { + LogVerbose(@"No space available to write..."); + + // No space available to write. + + if (![self usingCFStreamForTLS]) + { + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + } + return; + } + + if (flags & kStartingWriteTLS) + { + LogVerbose(@"Waiting for SSL/TLS handshake to complete"); + + // The writeQueue is waiting for SSL/TLS handshake to complete. + + if (flags & kStartingReadTLS) + { + if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) + { + // We are in the process of a SSL Handshake. + // We were waiting for available space in the socket's internal OS buffer to continue writing. + + [self ssl_continueSSLHandshake]; + } + } + else + { + // We are still waiting for the readQueue to drain and start the SSL/TLS process. + // We now know we can write to the socket. + + if (![self usingCFStreamForTLS]) + { + // Suspend the write source or else it will continue to fire nonstop. + + [self suspendWriteSource]; + } + } + + return; + } + + // Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet) + + BOOL waiting = NO; + NSError *error = nil; + size_t bytesWritten = 0; + + if (flags & kSocketSecure) + { + if ([self usingCFStreamForTLS]) + { + #if TARGET_OS_IPHONE + + // + // Writing data using CFStream (over internal TLS) + // + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite); + LogVerbose(@"CFWriteStreamWrite(%lu) = %li", (unsigned long)bytesToWrite, result); + + if (result < 0) + { + error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream); + } + else + { + bytesWritten = (size_t)result; + + // We always set waiting to true in this scenario. + // CFStream may have altered our underlying socket to non-blocking. + // Thus if we attempt to write without a callback, we may end up blocking our queue. + waiting = YES; + } + + #endif + } + else + { + // We're going to use the SSLWrite function. + // + // OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed) + // + // Parameters: + // context - An SSL session context reference. + // data - A pointer to the buffer of data to write. + // dataLength - The amount, in bytes, of data to write. + // processed - On return, the length, in bytes, of the data actually written. + // + // It sounds pretty straight-forward, + // but there are a few caveats you should be aware of. + // + // The SSLWrite method operates in a non-obvious (and rather annoying) manner. + // According to the documentation: + // + // Because you may configure the underlying connection to operate in a non-blocking manner, + // a write operation might return errSSLWouldBlock, indicating that less data than requested + // was actually transferred. In this case, you should repeat the call to SSLWrite until some + // other result is returned. + // + // This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock, + // then the SSLWrite method returns (with the proper errSSLWouldBlock return value), + // but it sets processed to dataLength !! + // + // In other words, if the SSLWrite function doesn't completely write all the data we tell it to, + // then it doesn't tell us how many bytes were actually written. So, for example, if we tell it to + // write 256 bytes then it might actually write 128 bytes, but then report 0 bytes written. + // + // You might be wondering: + // If the SSLWrite function doesn't tell us how many bytes were written, + // then how in the world are we supposed to update our parameters (buffer & bytesToWrite) + // for the next time we invoke SSLWrite? + // + // The answer is that SSLWrite cached all the data we told it to write, + // and it will push out that data next time we call SSLWrite. + // If we call SSLWrite with new data, it will push out the cached data first, and then the new data. + // If we call SSLWrite with empty data, then it will simply push out the cached data. + // + // For this purpose we're going to break large writes into a series of smaller writes. + // This allows us to report progress back to the delegate. + + OSStatus result; + + BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0); + BOOL hasNewDataToWrite = YES; + + if (hasCachedDataToWrite) + { + size_t processed = 0; + + result = SSLWrite(sslContext, NULL, 0, &processed); + + if (result == noErr) + { + bytesWritten = sslWriteCachedLength; + sslWriteCachedLength = 0; + + if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten)) + { + // We've written all data for the current write. + hasNewDataToWrite = NO; + } + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + } + else + { + error = [self sslError:result]; + } + + // Can't write any new data since we were unable to write the cached data. + hasNewDataToWrite = NO; + } + } + + if (hasNewDataToWrite) + { + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + + currentWrite->bytesDone + + bytesWritten; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + size_t bytesRemaining = bytesToWrite; + + BOOL keepLooping = YES; + while (keepLooping) + { + const size_t sslMaxBytesToWrite = 32768; + size_t sslBytesToWrite = MIN(bytesRemaining, sslMaxBytesToWrite); + size_t sslBytesWritten = 0; + + result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten); + + if (result == noErr) + { + buffer += sslBytesWritten; + bytesWritten += sslBytesWritten; + bytesRemaining -= sslBytesWritten; + + keepLooping = (bytesRemaining > 0); + } + else + { + if (result == errSSLWouldBlock) + { + waiting = YES; + sslWriteCachedLength = sslBytesToWrite; + } + else + { + error = [self sslError:result]; + } + + keepLooping = NO; + } + + } // while (keepLooping) + + } // if (hasNewDataToWrite) + } + } + else + { + // + // Writing data directly over raw socket + // + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; + + NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; + + if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) + { + bytesToWrite = SIZE_MAX; + } + + ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite); + LogVerbose(@"wrote to socket = %zd", result); + + // Check results + if (result < 0) + { + if (errno == EWOULDBLOCK) + { + waiting = YES; + } + else + { + error = [self errnoErrorWithReason:@"Error in write() function"]; + } + } + else + { + bytesWritten = result; + } + } + + // We're done with our writing. + // If we explictly ran into a situation where the socket told us there was no room in the buffer, + // then we immediately resume listening for notifications. + // + // We must do this before we dequeue another write, + // as that may in turn invoke this method again. + // + // Note that if CFStream is involved, it may have maliciously put our socket in blocking mode. + + if (waiting) + { + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + // Check our results + + BOOL done = NO; + + if (bytesWritten > 0) + { + // Update total amount read for the current write + currentWrite->bytesDone += bytesWritten; + LogVerbose(@"currentWrite->bytesDone = %lu", (unsigned long)currentWrite->bytesDone); + + // Is packet done? + done = (currentWrite->bytesDone == [currentWrite->buffer length]); + } + + if (done) + { + [self completeCurrentWrite]; + + if (!error) + { + dispatch_async(socketQueue, ^{ @autoreleasepool{ + + [self maybeDequeueWrite]; + }}); + } + } + else + { + // We were unable to finish writing the data, + // so we're waiting for another callback to notify us of available space in the lower-level output buffer. + + if (!waiting && !error) + { + // This would be the case if our write was able to accept some data, but not all of it. + + flags &= ~kSocketCanAcceptBytes; + + if (![self usingCFStreamForTLS]) + { + [self resumeWriteSource]; + } + } + + if (bytesWritten > 0) + { + // We're not done with the entire write, but we have written some bytes + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)]) + { + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag]; + }}); + } + } + } + + // Check for errors + + if (error) + { + [self closeWithError:[self errnoErrorWithReason:@"Error in write() function"]]; + } + + // Do not add any code here without first adding a return statement in the error case above. +} + +- (void)completeCurrentWrite +{ + LogTrace(); + + NSAssert(currentWrite, @"Trying to complete current write when there is no current write."); + + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWriteDataWithTag:)]) + { + long theWriteTag = currentWrite->tag; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didWriteDataWithTag:theWriteTag]; + }}); + } + + [self endCurrentWrite]; +} + +- (void)endCurrentWrite +{ + if (writeTimer) + { + dispatch_source_cancel(writeTimer); + writeTimer = NULL; + } + + currentWrite = nil; +} + +- (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) + { + writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + __weak GCDAsyncSocket *weakSelf = self; + + dispatch_source_set_event_handler(writeTimer, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf == nil) return_from_block; + + [strongSelf doWriteTimeout]; + + #pragma clang diagnostic pop + }}); + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theWriteTimer = writeTimer; + dispatch_source_set_cancel_handler(writeTimer, ^{ + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + LogVerbose(@"dispatch_release(writeTimer)"); + dispatch_release(theWriteTimer); + + #pragma clang diagnostic pop + }); + #endif + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(writeTimer); + } +} + +- (void)doWriteTimeout +{ + // This is a little bit tricky. + // Ideally we'd like to synchronously query the delegate about a timeout extension. + // But if we do so synchronously we risk a possible deadlock. + // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. + + flags |= kWritesPaused; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) + { + GCDAsyncWritePacket *theWrite = currentWrite; + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + NSTimeInterval timeoutExtension = 0.0; + + timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag + elapsed:theWrite->timeout + bytesDone:theWrite->bytesDone]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self doWriteTimeoutWithExtension:timeoutExtension]; + }}); + }}); + } + else + { + [self doWriteTimeoutWithExtension:0.0]; + } +} + +- (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension +{ + if (currentWrite) + { + if (timeoutExtension > 0.0) + { + currentWrite->timeout += timeoutExtension; + + // Reschedule the timer + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); + dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); + + // Unpause writes, and continue + flags &= ~kWritesPaused; + [self doWriteData]; + } + else + { + LogVerbose(@"WriteTimeout"); + + [self closeWithError:[self writeTimeoutError]]; + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)startTLS:(NSDictionary *)tlsSettings +{ + LogTrace(); + + if (tlsSettings == nil) + { + // Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary, + // but causes problems if we later try to fetch the remote host's certificate. + // + // To be exact, it causes the following to return NULL instead of the normal result: + // CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates) + // + // So we use an empty dictionary instead, which works perfectly. + + tlsSettings = [NSDictionary dictionary]; + } + + GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + if ((flags & kSocketStarted) && !(flags & kQueuedTLS) && !(flags & kForbidReadsWrites)) + { + [readQueue addObject:packet]; + [writeQueue addObject:packet]; + + flags |= kQueuedTLS; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + }}); + +} + +- (void)maybeStartTLS +{ + // We can't start TLS until: + // - All queued reads prior to the user calling startTLS are complete + // - All queued writes prior to the user calling startTLS are complete + // + // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + BOOL useSecureTransport = YES; + + #if TARGET_OS_IPHONE + { + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + NSDictionary *tlsSettings = tlsPacket->tlsSettings; + + NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS]; + if (value && [value boolValue]) + useSecureTransport = NO; + } + #endif + + if (useSecureTransport) + { + [self ssl_startTLS]; + } + else + { + #if TARGET_OS_IPHONE + [self cf_startTLS]; + #endif + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via SecureTransport +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength +{ + LogVerbose(@"sslReadWithBuffer:%p length:%lu", buffer, (unsigned long)*bufferLength); + + if ((socketFDBytesAvailable == 0) && ([sslPreBuffer availableBytes] == 0)) + { + LogVerbose(@"%@ - No data available to read...", THIS_METHOD); + + // No data available to read. + // + // Need to wait for readSource to fire and notify us of + // available data in the socket's internal read buffer. + + [self resumeReadSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t totalBytesRead = 0; + size_t totalBytesLeftToBeRead = *bufferLength; + + BOOL done = NO; + BOOL socketError = NO; + + // + // STEP 1 : READ FROM SSL PRE BUFFER + // + + size_t sslPreBufferLength = [sslPreBuffer availableBytes]; + + if (sslPreBufferLength > 0) + { + LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD); + + size_t bytesToCopy; + if (sslPreBufferLength > totalBytesLeftToBeRead) + bytesToCopy = totalBytesLeftToBeRead; + else + bytesToCopy = sslPreBufferLength; + + LogVerbose(@"%@: Copying %zu bytes from sslPreBuffer", THIS_METHOD, bytesToCopy); + + memcpy(buffer, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + + // + // STEP 2 : READ FROM SOCKET + // + + if (!done && (socketFDBytesAvailable > 0)) + { + LogVerbose(@"%@: Reading from socket...", THIS_METHOD); + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + BOOL readIntoPreBuffer; + size_t bytesToRead; + uint8_t *buf; + + if (socketFDBytesAvailable > totalBytesLeftToBeRead) + { + // Read all available data from socket into sslPreBuffer. + // Then copy requested amount into dataBuffer. + + LogVerbose(@"%@: Reading into sslPreBuffer...", THIS_METHOD); + + [sslPreBuffer ensureCapacityForWrite:socketFDBytesAvailable]; + + readIntoPreBuffer = YES; + bytesToRead = (size_t)socketFDBytesAvailable; + buf = [sslPreBuffer writeBuffer]; + } + else + { + // Read available data from socket directly into dataBuffer. + + LogVerbose(@"%@: Reading directly into dataBuffer...", THIS_METHOD); + + readIntoPreBuffer = NO; + bytesToRead = totalBytesLeftToBeRead; + buf = (uint8_t *)buffer + totalBytesRead; + } + + ssize_t result = read(socketFD, buf, bytesToRead); + LogVerbose(@"%@: read from socket = %zd", THIS_METHOD, result); + + if (result < 0) + { + LogVerbose(@"%@: read errno = %i", THIS_METHOD, errno); + + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + socketFDBytesAvailable = 0; + } + else if (result == 0) + { + LogVerbose(@"%@: read EOF", THIS_METHOD); + + socketError = YES; + socketFDBytesAvailable = 0; + } + else + { + size_t bytesReadFromSocket = result; + + if (socketFDBytesAvailable > bytesReadFromSocket) + socketFDBytesAvailable -= bytesReadFromSocket; + else + socketFDBytesAvailable = 0; + + if (readIntoPreBuffer) + { + [sslPreBuffer didWrite:bytesReadFromSocket]; + + size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket); + + LogVerbose(@"%@: Copying %zu bytes out of sslPreBuffer", THIS_METHOD, bytesToCopy); + + memcpy((uint8_t *)buffer + totalBytesRead, [sslPreBuffer readBuffer], bytesToCopy); + [sslPreBuffer didRead:bytesToCopy]; + + totalBytesRead += bytesToCopy; + totalBytesLeftToBeRead -= bytesToCopy; + + LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); + } + else + { + totalBytesRead += bytesReadFromSocket; + totalBytesLeftToBeRead -= bytesReadFromSocket; + } + + done = (totalBytesLeftToBeRead == 0); + + if (done) LogVerbose(@"%@: Complete", THIS_METHOD); + } + } + + *bufferLength = totalBytesRead; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +- (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength +{ + if (!(flags & kSocketCanAcceptBytes)) + { + // Unable to write. + // + // Need to wait for writeSource to fire and notify us of + // available space in the socket's internal write buffer. + + [self resumeWriteSource]; + + *bufferLength = 0; + return errSSLWouldBlock; + } + + size_t bytesToWrite = *bufferLength; + size_t bytesWritten = 0; + + BOOL done = NO; + BOOL socketError = NO; + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + ssize_t result = write(socketFD, buffer, bytesToWrite); + + if (result < 0) + { + if (errno != EWOULDBLOCK) + { + socketError = YES; + } + + flags &= ~kSocketCanAcceptBytes; + } + else if (result == 0) + { + flags &= ~kSocketCanAcceptBytes; + } + else + { + bytesWritten = result; + + done = (bytesWritten == bytesToWrite); + } + + *bufferLength = bytesWritten; + + if (done) + return noErr; + + if (socketError) + return errSSLClosedAbort; + + return errSSLWouldBlock; +} + +static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); + + return [asyncSocket sslReadWithBuffer:data length:dataLength]; +} + +static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; + + NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); + + return [asyncSocket sslWriteWithBuffer:data length:dataLength]; +} + +- (void)ssl_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via SecureTransport)..."); + + OSStatus status; + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + if (tlsPacket == nil) // Code to quiet the analyzer + { + NSAssert(NO, @"Logic error"); + + [self closeWithError:[self otherError:@"Logic error"]]; + return; + } + NSDictionary *tlsSettings = tlsPacket->tlsSettings; + + // Create SSLContext, and setup IO callbacks and connection ref + + BOOL isServer = [[tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer] boolValue]; + + #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) + { + if (isServer) + sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType); + else + sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType); + + if (sslContext == NULL) + { + [self closeWithError:[self otherError:@"Error in SSLCreateContext"]]; + return; + } + } + #else // (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) + { + status = SSLNewContext(isServer, &sslContext); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLNewContext"]]; + return; + } + } + #endif + + status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]]; + return; + } + + status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetConnection"]]; + return; + } + + + BOOL shouldManuallyEvaluateTrust = [[tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust] boolValue]; + if (shouldManuallyEvaluateTrust) + { + if (isServer) + { + [self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]]; + return; + } + + status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]]; + return; + } + + #if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) + + // Note from Apple's documentation: + // + // It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8. + // On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the + // built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus + // SSLSetEnableCertVerify is not available on that platform at all. + + status = SSLSetEnableCertVerify(sslContext, NO); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]]; + return; + } + + #endif + } + + // Configure SSLContext from given settings + // + // Checklist: + // 1. kCFStreamSSLPeerName + // 2. kCFStreamSSLCertificates + // 3. GCDAsyncSocketSSLPeerID + // 4. GCDAsyncSocketSSLProtocolVersionMin + // 5. GCDAsyncSocketSSLProtocolVersionMax + // 6. GCDAsyncSocketSSLSessionOptionFalseStart + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord + // 8. GCDAsyncSocketSSLCipherSuites + // 9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac) + // + // Deprecated (throw error): + // 10. kCFStreamSSLAllowsAnyRoot + // 11. kCFStreamSSLAllowsExpiredRoots + // 12. kCFStreamSSLAllowsExpiredCertificates + // 13. kCFStreamSSLValidatesCertificateChain + // 14. kCFStreamSSLLevel + + id value; + + // 1. kCFStreamSSLPeerName + + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLPeerName]; + if ([value isKindOfClass:[NSString class]]) + { + NSString *peerName = (NSString *)value; + + const char *peer = [peerName UTF8String]; + size_t peerLen = strlen(peer); + + status = SSLSetPeerDomainName(sslContext, peer, peerLen); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for kCFStreamSSLPeerName. Value must be of type NSString."); + + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLPeerName."]]; + return; + } + + // 2. kCFStreamSSLCertificates + + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLCertificates]; + if ([value isKindOfClass:[NSArray class]]) + { + CFArrayRef certs = (__bridge CFArrayRef)value; + + status = SSLSetCertificate(sslContext, certs); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetCertificate"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for kCFStreamSSLCertificates. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLCertificates."]]; + return; + } + + // 3. GCDAsyncSocketSSLPeerID + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLPeerID]; + if ([value isKindOfClass:[NSData class]]) + { + NSData *peerIdData = (NSData *)value; + + status = SSLSetPeerID(sslContext, [peerIdData bytes], [peerIdData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetPeerID"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLPeerID. Value must be of type NSData." + @" (You can convert strings to data using a method like" + @" [string dataUsingEncoding:NSUTF8StringEncoding])"); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLPeerID."]]; + return; + } + + // 4. GCDAsyncSocketSSLProtocolVersionMin + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMin]; + if ([value isKindOfClass:[NSNumber class]]) + { + SSLProtocol minProtocol = (SSLProtocol)[(NSNumber *)value intValue]; + if (minProtocol != kSSLProtocolUnknown) + { + status = SSLSetProtocolVersionMin(sslContext, minProtocol); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMin"]]; + return; + } + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMin. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMin."]]; + return; + } + + // 5. GCDAsyncSocketSSLProtocolVersionMax + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMax]; + if ([value isKindOfClass:[NSNumber class]]) + { + SSLProtocol maxProtocol = (SSLProtocol)[(NSNumber *)value intValue]; + if (maxProtocol != kSSLProtocolUnknown) + { + status = SSLSetProtocolVersionMax(sslContext, maxProtocol); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMax"]]; + return; + } + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMax. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMax."]]; + return; + } + + // 6. GCDAsyncSocketSSLSessionOptionFalseStart + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionFalseStart]; + if ([value isKindOfClass:[NSNumber class]]) + { + status = SSLSetSessionOption(sslContext, kSSLSessionOptionFalseStart, [value boolValue]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionFalseStart)"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart. Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart."]]; + return; + } + + // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionSendOneByteRecord]; + if ([value isKindOfClass:[NSNumber class]]) + { + status = SSLSetSessionOption(sslContext, kSSLSessionOptionSendOneByteRecord, [value boolValue]); + if (status != noErr) + { + [self closeWithError: + [self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionSendOneByteRecord)"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord." + @" Value must be of type NSNumber."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord."]]; + return; + } + + // 8. GCDAsyncSocketSSLCipherSuites + + value = [tlsSettings objectForKey:GCDAsyncSocketSSLCipherSuites]; + if ([value isKindOfClass:[NSArray class]]) + { + NSArray *cipherSuites = (NSArray *)value; + NSUInteger numberCiphers = [cipherSuites count]; + SSLCipherSuite ciphers[numberCiphers]; + + NSUInteger cipherIndex; + for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++) + { + NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex]; + ciphers[cipherIndex] = [cipherObject shortValue]; + } + + status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetEnabledCiphers"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLCipherSuites. Value must be of type NSArray."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLCipherSuites."]]; + return; + } + + // 9. GCDAsyncSocketSSLDiffieHellmanParameters + + #if !TARGET_OS_IPHONE + value = [tlsSettings objectForKey:GCDAsyncSocketSSLDiffieHellmanParameters]; + if ([value isKindOfClass:[NSData class]]) + { + NSData *diffieHellmanData = (NSData *)value; + + status = SSLSetDiffieHellmanParams(sslContext, [diffieHellmanData bytes], [diffieHellmanData length]); + if (status != noErr) + { + [self closeWithError:[self otherError:@"Error in SSLSetDiffieHellmanParams"]]; + return; + } + } + else if (value) + { + NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters. Value must be of type NSData."); + + [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters."]]; + return; + } + #endif + + // DEPRECATED checks + + // 10. kCFStreamSSLAllowsAnyRoot + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsAnyRoot]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsAnyRoot" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsAnyRoot"]]; + return; + } + + // 11. kCFStreamSSLAllowsExpiredRoots + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredRoots]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredRoots" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredRoots"]]; + return; + } + + // 12. kCFStreamSSLValidatesCertificateChain + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLValidatesCertificateChain]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLValidatesCertificateChain" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLValidatesCertificateChain"]]; + return; + } + + // 13. kCFStreamSSLAllowsExpiredCertificates + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredCertificates]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates" + @" - You must use manual trust evaluation"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates"]]; + return; + } + + // 14. kCFStreamSSLLevel + + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLLevel]; + #pragma clang diagnostic pop + if (value) + { + NSAssert(NO, @"Security option unavailable - kCFStreamSSLLevel" + @" - You must use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMax"); + + [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLLevel"]]; + return; + } + + // Setup the sslPreBuffer + // + // Any data in the preBuffer needs to be moved into the sslPreBuffer, + // as this data is now part of the secure read stream. + + sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; + + size_t preBufferLength = [preBuffer availableBytes]; + + if (preBufferLength > 0) + { + [sslPreBuffer ensureCapacityForWrite:preBufferLength]; + + memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength); + [preBuffer didRead:preBufferLength]; + [sslPreBuffer didWrite:preBufferLength]; + } + + sslErrCode = lastSSLHandshakeError = noErr; + + // Start the SSL Handshake process + + [self ssl_continueSSLHandshake]; +} + +- (void)ssl_continueSSLHandshake +{ + LogTrace(); + + // If the return value is noErr, the session is ready for normal secure communication. + // If the return value is errSSLWouldBlock, the SSLHandshake function must be called again. + // If the return value is errSSLServerAuthCompleted, we ask delegate if we should trust the + // server and then call SSLHandshake again to resume the handshake or close the connection + // errSSLPeerBadCert SSL error. + // Otherwise, the return value indicates an error code. + + OSStatus status = SSLHandshake(sslContext); + lastSSLHandshakeError = status; + + if (status == noErr) + { + LogVerbose(@"SSLHandshake complete"); + + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } + else if (status == errSSLPeerAuthCompleted) + { + LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval"); + + __block SecTrustRef trust = NULL; + status = SSLCopyPeerTrust(sslContext, &trust); + if (status != noErr) + { + [self closeWithError:[self sslError:status]]; + return; + } + + int aStateIndex = stateIndex; + dispatch_queue_t theSocketQueue = socketQueue; + + __weak GCDAsyncSocket *weakSelf = self; + + void (^comletionHandler)(BOOL) = ^(BOOL shouldTrust){ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + dispatch_async(theSocketQueue, ^{ @autoreleasepool { + + if (trust) { + CFRelease(trust); + trust = NULL; + } + + __strong GCDAsyncSocket *strongSelf = weakSelf; + if (strongSelf) + { + [strongSelf ssl_shouldTrustPeer:shouldTrust stateIndex:aStateIndex]; + } + }}); + + #pragma clang diagnostic pop + }}; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReceiveTrust:completionHandler:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler]; + }}); + } + else + { + if (trust) { + CFRelease(trust); + trust = NULL; + } + + NSString *msg = @"GCDAsyncSocketManuallyEvaluateTrust specified in tlsSettings," + @" but delegate doesn't implement socket:shouldTrustPeer:"; + + [self closeWithError:[self otherError:msg]]; + return; + } + } + else if (status == errSSLWouldBlock) + { + LogVerbose(@"SSLHandshake continues..."); + + // Handshake continues... + // + // This method will be called again from doReadData or doWriteData. + } + else + { + [self closeWithError:[self sslError:status]]; + } +} + +- (void)ssl_shouldTrustPeer:(BOOL)shouldTrust stateIndex:(int)aStateIndex +{ + LogTrace(); + + if (aStateIndex != stateIndex) + { + LogInfo(@"Ignoring ssl_shouldTrustPeer - invalid state (maybe disconnected)"); + + // One of the following is true + // - the socket was disconnected + // - the startTLS operation timed out + // - the completionHandler was already invoked once + + return; + } + + // Increment stateIndex to ensure completionHandler can only be called once. + stateIndex++; + + if (shouldTrust) + { + NSAssert(lastSSLHandshakeError == errSSLPeerAuthCompleted, @"ssl_shouldTrustPeer called when last error is %d and not errSSLPeerAuthCompleted", (int)lastSSLHandshakeError); + [self ssl_continueSSLHandshake]; + } + else + { + [self closeWithError:[self sslError:errSSLPeerBadCert]]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security via CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + +- (void)cf_finishSSLHandshake +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + flags |= kSocketSecure; + + __strong id theDelegate = delegate; + + if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate socketDidSecure:self]; + }}); + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } +} + +- (void)cf_abortSSLHandshake:(NSError *)error +{ + LogTrace(); + + if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) + { + flags &= ~kStartingReadTLS; + flags &= ~kStartingWriteTLS; + + [self closeWithError:error]; + } +} + +- (void)cf_startTLS +{ + LogTrace(); + + LogVerbose(@"Starting TLS (via CFStream)..."); + + if ([preBuffer availableBytes] > 0) + { + NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket."; + + [self closeWithError:[self otherError:msg]]; + return; + } + + [self suspendReadSource]; + [self suspendWriteSource]; + + socketFDBytesAvailable = 0; + flags &= ~kSocketCanAcceptBytes; + flags &= ~kSecureSocketHasBytesAvailable; + + flags |= kUsingCFStreamForTLS; + + if (![self createReadAndWriteStream]) + { + [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]]; + return; + } + + if (![self registerForStreamCallbacksIncludingReadWrite:YES]) + { + [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; + return; + } + + if (![self addStreamsToRunLoop]) + { + [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; + return; + } + + NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS"); + NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS"); + + GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; + CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings; + + // Getting an error concerning kCFStreamPropertySSLSettings ? + // You need to add the CFNetwork framework to your iOS application. + + BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings); + BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings); + + // For some reason, starting around the time of iOS 4.3, + // the first call to set the kCFStreamPropertySSLSettings will return true, + // but the second will return false. + // + // Order doesn't seem to matter. + // So you could call CFReadStreamSetProperty and then CFWriteStreamSetProperty, or you could reverse the order. + // Either way, the first call will return true, and the second returns false. + // + // Interestingly, this doesn't seem to affect anything. + // Which is not altogether unusual, as the documentation seems to suggest that (for many settings) + // setting it on one side of the stream automatically sets it for the other side of the stream. + // + // Although there isn't anything in the documentation to suggest that the second attempt would fail. + // + // Furthermore, this only seems to affect streams that are negotiating a security upgrade. + // In other words, the socket gets connected, there is some back-and-forth communication over the unsecure + // connection, and then a startTLS is issued. + // So this mostly affects newer protocols (XMPP, IMAP) as opposed to older protocols (HTTPS). + + if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug. + { + [self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]]; + return; + } + + if (![self openStreams]) + { + [self closeWithError:[self otherError:@"Error in CFStreamOpen"]]; + return; + } + + LogVerbose(@"Waiting for SSL Handshake to complete..."); +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + ++ (void)ignore:(id)_ +{} + ++ (void)startCFStreamThreadIfNeeded +{ + LogTrace(); + + static dispatch_once_t predicate; + dispatch_once(&predicate, ^{ + + cfstreamThreadRetainCount = 0; + cfstreamThreadSetupQueue = dispatch_queue_create("GCDAsyncSocket-CFStreamThreadSetup", DISPATCH_QUEUE_SERIAL); + }); + + dispatch_sync(cfstreamThreadSetupQueue, ^{ @autoreleasepool { + + if (++cfstreamThreadRetainCount == 1) + { + cfstreamThread = [[NSThread alloc] initWithTarget:self + selector:@selector(cfstreamThread) + object:nil]; + [cfstreamThread start]; + } + }}); +} + ++ (void)stopCFStreamThreadIfNeeded +{ + LogTrace(); + + // The creation of the cfstreamThread is relatively expensive. + // So we'd like to keep it available for recycling. + // However, there's a tradeoff here, because it shouldn't remain alive forever. + // So what we're going to do is use a little delay before taking it down. + // This way it can be reused properly in situations where multiple sockets are continually in flux. + + int delayInSeconds = 30; + dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); + dispatch_after(when, cfstreamThreadSetupQueue, ^{ @autoreleasepool { + #pragma clang diagnostic push + #pragma clang diagnostic warning "-Wimplicit-retain-self" + + if (cfstreamThreadRetainCount == 0) + { + LogWarn(@"Logic error concerning cfstreamThread start / stop"); + return_from_block; + } + + if (--cfstreamThreadRetainCount == 0) + { + [cfstreamThread cancel]; // set isCancelled flag + + // wake up the thread + [[self class] performSelector:@selector(ignore:) + onThread:cfstreamThread + withObject:[NSNull null] + waitUntilDone:NO]; + + cfstreamThread = nil; + } + + #pragma clang diagnostic pop + }}); +} + ++ (void)cfstreamThread { @autoreleasepool +{ + [[NSThread currentThread] setName:GCDAsyncSocketThreadName]; + + LogInfo(@"CFStreamThread: Started"); + + // We can't run the run loop unless it has an associated input source or a timer. + // So we'll just create a timer that will never fire - unless the server runs for decades. + [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] + target:self + selector:@selector(ignore:) + userInfo:nil + repeats:YES]; + + NSThread *currentThread = [NSThread currentThread]; + NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop]; + + BOOL isCancelled = [currentThread isCancelled]; + + while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) + { + isCancelled = [currentThread isCancelled]; + } + + LogInfo(@"CFStreamThread: Stopped"); +}} + ++ (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); +} + ++ (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncSocket->readStream) + CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); + + if (asyncSocket->writeStream) + CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); +} + +static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + + switch(type) + { + case kCFStreamEventHasBytesAvailable: + { + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFReadStreamHasBytesAvailable(asyncSocket->readStream)) + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSecureSocketHasBytesAvailable; + [asyncSocket doReadData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - Other"); + + if (asyncSocket->readStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } + +} + +static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) +{ + GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; + + switch(type) + { + case kCFStreamEventCanAcceptBytes: + { + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. + // (A callback related to the tcp stream, but not to the SSL layer). + + if (CFWriteStreamCanAcceptBytes(asyncSocket->writeStream)) + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket cf_finishSSLHandshake]; + } + } + else + { + asyncSocket->flags |= kSocketCanAcceptBytes; + [asyncSocket doWriteData]; + } + }}); + + break; + } + default: + { + NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); + + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncSocket connectionClosedError]; + } + + dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - Other"); + + if (asyncSocket->writeStream != stream) + return_from_block; + + if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) + { + [asyncSocket cf_abortSSLHandshake:error]; + } + else + { + [asyncSocket closeWithError:error]; + } + }}); + + break; + } + } + +} + +- (BOOL)createReadAndWriteStream +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + if (readStream || writeStream) + { + // Streams already created + return YES; + } + + int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; + + if (socketFD == SOCKET_NULL) + { + // Cannot create streams without a file descriptor + return NO; + } + + if (![self isConnected]) + { + // Cannot create streams until file descriptor is connected + return NO; + } + + LogVerbose(@"Creating read and write stream..."); + + CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream); + + // The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case). + // But let's not take any chances. + + if (readStream) + CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + if (writeStream) + CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + + if ((readStream == NULL) || (writeStream == NULL)) + { + LogWarn(@"Unable to create read and write stream..."); + + if (readStream) + { + CFReadStreamClose(readStream); + CFRelease(readStream); + readStream = NULL; + } + if (writeStream) + { + CFWriteStreamClose(writeStream); + CFRelease(writeStream); + writeStream = NULL; + } + + return NO; + } + + return YES; +} + +- (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite +{ + LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO")); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + streamContext.version = 0; + streamContext.info = (__bridge void *)(self); + streamContext.retain = nil; + streamContext.release = nil; + streamContext.copyDescription = nil; + + CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + readStreamEvents |= kCFStreamEventHasBytesAvailable; + + if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext)) + { + return NO; + } + + CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (includeReadWrite) + writeStreamEvents |= kCFStreamEventCanAcceptBytes; + + if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext)) + { + return NO; + } + + return YES; +} + +- (BOOL)addStreamsToRunLoop +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + if (!(flags & kAddedStreamsToRunLoop)) + { + LogVerbose(@"Adding streams to runloop..."); + + [[self class] startCFStreamThreadIfNeeded]; + [[self class] performSelector:@selector(scheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + + flags |= kAddedStreamsToRunLoop; + } + + return YES; +} + +- (void)removeStreamsFromRunLoop +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + if (flags & kAddedStreamsToRunLoop) + { + LogVerbose(@"Removing streams from runloop..."); + + [[self class] performSelector:@selector(unscheduleCFStreams:) + onThread:cfstreamThread + withObject:self + waitUntilDone:YES]; + [[self class] stopCFStreamThreadIfNeeded]; + + flags &= ~kAddedStreamsToRunLoop; + } +} + +- (BOOL)openStreams +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); + + CFStreamStatus readStatus = CFReadStreamGetStatus(readStream); + CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream); + + if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen)) + { + LogVerbose(@"Opening read and write stream..."); + + BOOL r1 = CFReadStreamOpen(readStream); + BOOL r2 = CFWriteStreamOpen(writeStream); + + if (!r1 || !r2) + { + LogError(@"Error in CFStreamOpen"); + return NO; + } + } + + return YES; +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Advanced +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * See header file for big discussion of this method. +**/ +- (BOOL)autoDisconnectOnClosedReadStream +{ + // Note: YES means kAllowHalfDuplexConnection is OFF + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return ((config & kAllowHalfDuplexConnection) == 0); + } + else + { + __block BOOL result; + + dispatch_sync(socketQueue, ^{ + result = ((config & kAllowHalfDuplexConnection) == 0); + }); + + return result; + } +} + +/** + * See header file for big discussion of this method. +**/ +- (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag +{ + // Note: YES means kAllowHalfDuplexConnection is OFF + + dispatch_block_t block = ^{ + + if (flag) + config &= ~kAllowHalfDuplexConnection; + else + config |= kAllowHalfDuplexConnection; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + + +/** + * See header file for big discussion of this method. +**/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue +{ + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); +} + +/** + * See header file for big discussion of this method. +**/ +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue +{ + dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL); +} + +/** + * See header file for big discussion of this method. +**/ +- (void)performBlock:(dispatch_block_t)block +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socketFD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + if (socket4FD != SOCKET_NULL) + return socket4FD; + else + return socket6FD; +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socket4FD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + return socket4FD; +} + +/** + * Questions? Have you read the header file? +**/ +- (int)socket6FD +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return SOCKET_NULL; + } + + return socket6FD; +} + +#if TARGET_OS_IPHONE + +/** + * Questions? Have you read the header file? +**/ +- (CFReadStreamRef)readStream +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + if (readStream == NULL) + [self createReadAndWriteStream]; + + return readStream; +} + +/** + * Questions? Have you read the header file? +**/ +- (CFWriteStreamRef)writeStream +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + if (writeStream == NULL) + [self createReadAndWriteStream]; + + return writeStream; +} + +- (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat +{ + if (![self createReadAndWriteStream]) + { + // Error occurred creating streams (perhaps socket isn't open) + return NO; + } + + BOOL r1, r2; + + LogVerbose(@"Enabling backgrouding on socket"); + + r1 = CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + r2 = CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + + if (!r1 || !r2) + { + return NO; + } + + if (!caveat) + { + if (![self openStreams]) + { + return NO; + } + } + + return YES; +} + +/** + * Questions? Have you read the header file? +**/ +- (BOOL)enableBackgroundingOnSocket +{ + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:NO]; +} + +- (BOOL)enableBackgroundingOnSocketWithCaveat // Deprecated in iOS 4.??? +{ + // This method was created as a workaround for a bug in iOS. + // Apple has since fixed this bug. + // I'm not entirely sure which version of iOS they fixed it in... + + LogTrace(); + + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NO; + } + + return [self enableBackgroundingOnSocketWithCaveat:YES]; +} + +#endif + +- (SSLContextRef)sslContext +{ + if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); + return NULL; + } + + return sslContext; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Class Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr +{ + LogTrace(); + + NSMutableArray *addresses = nil; + NSError *error = nil; + + if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) + { + // Use LOOPBACK address + struct sockaddr_in nativeAddr4; + nativeAddr4.sin_len = sizeof(struct sockaddr_in); + nativeAddr4.sin_family = AF_INET; + nativeAddr4.sin_port = htons(port); + nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_loopback; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures + + NSData *address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + NSData *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + + addresses = [NSMutableArray arrayWithCapacity:2]; + [addresses addObject:address4]; + [addresses addObject:address6]; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + + int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); + + if (gai_error) + { + error = [self gaiError:gai_error]; + } + else + { + NSUInteger capacity = 0; + for (res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET || res->ai_family == AF_INET6) { + capacity++; + } + } + + addresses = [NSMutableArray arrayWithCapacity:capacity]; + + for (res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET) + { + // Found IPv4 address. + // Wrap the native address structure, and add to results. + + NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + [addresses addObject:address4]; + } + else if (res->ai_family == AF_INET6) + { + // Found IPv6 address. + // Wrap the native address structure, and add to results. + + NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + [addresses addObject:address6]; + } + } + freeaddrinfo(res0); + + if ([addresses count] == 0) + { + error = [self gaiError:EAI_FAIL]; + } + } + } + + if (errPtr) *errPtr = error; + return addresses; +} + ++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + char addrBuf[INET_ADDRSTRLEN]; + + if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + char addrBuf[INET6_ADDRSTRLEN]; + + if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + return ntohs(pSockaddr4->sin_port); +} + ++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + return ntohs(pSockaddr6->sin6_port); +} + ++ (NSURL *)urlFromSockaddrUN:(const struct sockaddr_un *)pSockaddr +{ + NSString *path = [NSString stringWithUTF8String:pSockaddr->sun_path]; + return [NSURL fileURLWithPath:path]; +} + ++ (NSString *)hostFromAddress:(NSData *)address +{ + NSString *host; + + if ([self getHost:&host port:NULL fromAddress:address]) + return host; + else + return nil; +} + ++ (uint16_t)portFromAddress:(NSData *)address +{ + uint16_t port; + + if ([self getHost:NULL port:&port fromAddress:address]) + return port; + else + return 0; +} + ++ (BOOL)isIPv4Address:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET) { + return YES; + } + } + + return NO; +} + ++ (BOOL)isIPv6Address:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET6) { + return YES; + } + } + + return NO; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address +{ + return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_t *)afPtr fromAddress:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *sockaddrX = [address bytes]; + + if (sockaddrX->sa_family == AF_INET) + { + if ([address length] >= sizeof(struct sockaddr_in)) + { + struct sockaddr_in sockaddr4; + memcpy(&sockaddr4, sockaddrX, sizeof(sockaddr4)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr4:&sockaddr4]; + if (portPtr) *portPtr = [self portFromSockaddr4:&sockaddr4]; + if (afPtr) *afPtr = AF_INET; + + return YES; + } + } + else if (sockaddrX->sa_family == AF_INET6) + { + if ([address length] >= sizeof(struct sockaddr_in6)) + { + struct sockaddr_in6 sockaddr6; + memcpy(&sockaddr6, sockaddrX, sizeof(sockaddr6)); + + if (hostPtr) *hostPtr = [self hostFromSockaddr6:&sockaddr6]; + if (portPtr) *portPtr = [self portFromSockaddr6:&sockaddr6]; + if (afPtr) *afPtr = AF_INET6; + + return YES; + } + } + } + + return NO; +} + ++ (NSData *)CRLFData +{ + return [NSData dataWithBytes:"\x0D\x0A" length:2]; +} + ++ (NSData *)CRData +{ + return [NSData dataWithBytes:"\x0D" length:1]; +} + ++ (NSData *)LFData +{ + return [NSData dataWithBytes:"\x0A" length:1]; +} + ++ (NSData *)ZeroData +{ + return [NSData dataWithBytes:"" length:1]; +} + +@end diff --git a/Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncUdpSocket.h b/Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncUdpSocket.h new file mode 100644 index 0000000..7587965 --- /dev/null +++ b/Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncUdpSocket.h @@ -0,0 +1,1007 @@ +// +// GCDAsyncUdpSocket +// +// This class is in the public domain. +// Originally created by Robbie Hanson of Deusty LLC. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import +#import +#import +#import + +extern NSString *const GCDAsyncUdpSocketException; +extern NSString *const GCDAsyncUdpSocketErrorDomain; + +extern NSString *const GCDAsyncUdpSocketQueueName; +extern NSString *const GCDAsyncUdpSocketThreadName; + +typedef NS_ENUM(NSInteger, GCDAsyncUdpSocketError) { + GCDAsyncUdpSocketNoError = 0, // Never used + GCDAsyncUdpSocketBadConfigError, // Invalid configuration + GCDAsyncUdpSocketBadParamError, // Invalid parameter was passed + GCDAsyncUdpSocketSendTimeoutError, // A send operation timed out + GCDAsyncUdpSocketClosedError, // The socket was closed + GCDAsyncUdpSocketOtherError, // Description provided in userInfo +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@class GCDAsyncUdpSocket; + +@protocol GCDAsyncUdpSocketDelegate +@optional + +/** + * By design, UDP is a connectionless protocol, and connecting is not needed. + * However, you may optionally choose to connect to a particular host for reasons + * outlined in the documentation for the various connect methods listed above. + * + * This method is called if one of the connect methods are invoked, and the connection is successful. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didConnectToAddress:(NSData *)address; + +/** + * By design, UDP is a connectionless protocol, and connecting is not needed. + * However, you may optionally choose to connect to a particular host for reasons + * outlined in the documentation for the various connect methods listed above. + * + * This method is called if one of the connect methods are invoked, and the connection fails. + * This may happen, for example, if a domain name is given for the host and the domain name is unable to be resolved. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotConnect:(NSError *)error; + +/** + * Called when the datagram with the given tag has been sent. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didSendDataWithTag:(long)tag; + +/** + * Called if an error occurs while trying to send a datagram. + * This could be due to a timeout, or something more serious such as the data being too large to fit in a sigle packet. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotSendDataWithTag:(long)tag dueToError:(NSError *)error; + +/** + * Called when the socket has received the requested datagram. +**/ +- (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data + fromAddress:(NSData *)address + withFilterContext:(id)filterContext; + +/** + * Called when the socket is closed. +**/ +- (void)udpSocketDidClose:(GCDAsyncUdpSocket *)sock withError:(NSError *)error; + +@end + +/** + * You may optionally set a receive filter for the socket. + * A filter can provide several useful features: + * + * 1. Many times udp packets need to be parsed. + * Since the filter can run in its own independent queue, you can parallelize this parsing quite easily. + * The end result is a parallel socket io, datagram parsing, and packet processing. + * + * 2. Many times udp packets are discarded because they are duplicate/unneeded/unsolicited. + * The filter can prevent such packets from arriving at the delegate. + * And because the filter can run in its own independent queue, this doesn't slow down the delegate. + * + * - Since the udp protocol does not guarantee delivery, udp packets may be lost. + * Many protocols built atop udp thus provide various resend/re-request algorithms. + * This sometimes results in duplicate packets arriving. + * A filter may allow you to architect the duplicate detection code to run in parallel to normal processing. + * + * - Since the udp socket may be connectionless, its possible for unsolicited packets to arrive. + * Such packets need to be ignored. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * @param data - The packet that was received. + * @param address - The address the data was received from. + * See utilities section for methods to extract info from address. + * @param context - Out parameter you may optionally set, which will then be passed to the delegate method. + * For example, filter block can parse the data and then, + * pass the parsed data to the delegate. + * + * @returns - YES if the received packet should be passed onto the delegate. + * NO if the received packet should be discarded, and not reported to the delegete. + * + * Example: + * + * GCDAsyncUdpSocketReceiveFilterBlock filter = ^BOOL (NSData *data, NSData *address, id *context) { + * + * MyProtocolMessage *msg = [MyProtocol parseMessage:data]; + * + * *context = response; + * return (response != nil); + * }; + * [udpSocket setReceiveFilter:filter withQueue:myParsingQueue]; + * +**/ +typedef BOOL (^GCDAsyncUdpSocketReceiveFilterBlock)(NSData *data, NSData *address, id *context); + +/** + * You may optionally set a send filter for the socket. + * A filter can provide several interesting possibilities: + * + * 1. Optional caching of resolved addresses for domain names. + * The cache could later be consulted, resulting in fewer system calls to getaddrinfo. + * + * 2. Reusable modules of code for bandwidth monitoring. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * @param data - The packet that was received. + * @param address - The address the data was received from. + * See utilities section for methods to extract info from address. + * @param tag - The tag that was passed in the send method. + * + * @returns - YES if the packet should actually be sent over the socket. + * NO if the packet should be silently dropped (not sent over the socket). + * + * Regardless of the return value, the delegate will be informed that the packet was successfully sent. + * +**/ +typedef BOOL (^GCDAsyncUdpSocketSendFilterBlock)(NSData *data, NSData *address, long tag); + + +@interface GCDAsyncUdpSocket : NSObject + +/** + * GCDAsyncUdpSocket uses the standard delegate paradigm, + * but executes all delegate callbacks on a given delegate dispatch queue. + * This allows for maximum concurrency, while at the same time providing easy thread safety. + * + * You MUST set a delegate AND delegate dispatch queue before attempting to + * use the socket, or you will get an error. + * + * The socket queue is optional. + * If you pass NULL, GCDAsyncSocket will automatically create its own socket queue. + * If you choose to provide a socket queue, the socket queue must not be a concurrent queue, + * then please see the discussion for the method markSocketQueueTargetQueue. + * + * The delegate queue and socket queue can optionally be the same. +**/ +- (id)init; +- (id)initWithSocketQueue:(dispatch_queue_t)sq; +- (id)initWithDelegate:(id )aDelegate delegateQueue:(dispatch_queue_t)dq; +- (id)initWithDelegate:(id )aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq; + +#pragma mark Configuration + +- (id )delegate; +- (void)setDelegate:(id )delegate; +- (void)synchronouslySetDelegate:(id )delegate; + +- (dispatch_queue_t)delegateQueue; +- (void)setDelegateQueue:(dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)delegateQueue; + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr; +- (void)setDelegate:(id )delegate delegateQueue:(dispatch_queue_t)delegateQueue; +- (void)synchronouslySetDelegate:(id )delegate delegateQueue:(dispatch_queue_t)delegateQueue; + +/** + * By default, both IPv4 and IPv6 are enabled. + * + * This means GCDAsyncUdpSocket automatically supports both protocols, + * and can send to IPv4 or IPv6 addresses, + * as well as receive over IPv4 and IPv6. + * + * For operations that require DNS resolution, GCDAsyncUdpSocket supports both IPv4 and IPv6. + * If a DNS lookup returns only IPv4 results, GCDAsyncUdpSocket will automatically use IPv4. + * If a DNS lookup returns only IPv6 results, GCDAsyncUdpSocket will automatically use IPv6. + * If a DNS lookup returns both IPv4 and IPv6 results, then the protocol used depends on the configured preference. + * If IPv4 is preferred, then IPv4 is used. + * If IPv6 is preferred, then IPv6 is used. + * If neutral, then the first IP version in the resolved array will be used. + * + * Starting with Mac OS X 10.7 Lion and iOS 5, the default IP preference is neutral. + * On prior systems the default IP preference is IPv4. + **/ +- (BOOL)isIPv4Enabled; +- (void)setIPv4Enabled:(BOOL)flag; + +- (BOOL)isIPv6Enabled; +- (void)setIPv6Enabled:(BOOL)flag; + +- (BOOL)isIPv4Preferred; +- (BOOL)isIPv6Preferred; +- (BOOL)isIPVersionNeutral; + +- (void)setPreferIPv4; +- (void)setPreferIPv6; +- (void)setIPVersionNeutral; + +/** + * Gets/Sets the maximum size of the buffer that will be allocated for receive operations. + * The default maximum size is 9216 bytes. + * + * The theoretical maximum size of any IPv4 UDP packet is UINT16_MAX = 65535. + * The theoretical maximum size of any IPv6 UDP packet is UINT32_MAX = 4294967295. + * + * Since the OS/GCD notifies us of the size of each received UDP packet, + * the actual allocated buffer size for each packet is exact. + * And in practice the size of UDP packets is generally much smaller than the max. + * Indeed most protocols will send and receive packets of only a few bytes, + * or will set a limit on the size of packets to prevent fragmentation in the IP layer. + * + * If you set the buffer size too small, the sockets API in the OS will silently discard + * any extra data, and you will not be notified of the error. +**/ +- (uint16_t)maxReceiveIPv4BufferSize; +- (void)setMaxReceiveIPv4BufferSize:(uint16_t)max; + +- (uint32_t)maxReceiveIPv6BufferSize; +- (void)setMaxReceiveIPv6BufferSize:(uint32_t)max; + +/** + * User data allows you to associate arbitrary information with the socket. + * This data is not used internally in any way. +**/ +- (id)userData; +- (void)setUserData:(id)arbitraryUserData; + +#pragma mark Diagnostics + +/** + * Returns the local address info for the socket. + * + * The localAddress method returns a sockaddr structure wrapped in a NSData object. + * The localHost method returns the human readable IP address as a string. + * + * Note: Address info may not be available until after the socket has been binded, connected + * or until after data has been sent. +**/ +- (NSData *)localAddress; +- (NSString *)localHost; +- (uint16_t)localPort; + +- (NSData *)localAddress_IPv4; +- (NSString *)localHost_IPv4; +- (uint16_t)localPort_IPv4; + +- (NSData *)localAddress_IPv6; +- (NSString *)localHost_IPv6; +- (uint16_t)localPort_IPv6; + +/** + * Returns the remote address info for the socket. + * + * The connectedAddress method returns a sockaddr structure wrapped in a NSData object. + * The connectedHost method returns the human readable IP address as a string. + * + * Note: Since UDP is connectionless by design, connected address info + * will not be available unless the socket is explicitly connected to a remote host/port. + * If the socket is not connected, these methods will return nil / 0. +**/ +- (NSData *)connectedAddress; +- (NSString *)connectedHost; +- (uint16_t)connectedPort; + +/** + * Returns whether or not this socket has been connected to a single host. + * By design, UDP is a connectionless protocol, and connecting is not needed. + * If connected, the socket will only be able to send/receive data to/from the connected host. +**/ +- (BOOL)isConnected; + +/** + * Returns whether or not this socket has been closed. + * The only way a socket can be closed is if you explicitly call one of the close methods. +**/ +- (BOOL)isClosed; + +/** + * Returns whether or not this socket is IPv4. + * + * By default this will be true, unless: + * - IPv4 is disabled (via setIPv4Enabled:) + * - The socket is explicitly bound to an IPv6 address + * - The socket is connected to an IPv6 address +**/ +- (BOOL)isIPv4; + +/** + * Returns whether or not this socket is IPv6. + * + * By default this will be true, unless: + * - IPv6 is disabled (via setIPv6Enabled:) + * - The socket is explicitly bound to an IPv4 address + * _ The socket is connected to an IPv4 address + * + * This method will also return false on platforms that do not support IPv6. + * Note: The iPhone does not currently support IPv6. +**/ +- (BOOL)isIPv6; + +#pragma mark Binding + +/** + * Binds the UDP socket to the given port. + * Binding should be done for server sockets that receive data prior to sending it. + * Client sockets can skip binding, + * as the OS will automatically assign the socket an available port when it starts sending data. + * + * You may optionally pass a port number of zero to immediately bind the socket, + * yet still allow the OS to automatically assign an available port. + * + * You cannot bind a socket after its been connected. + * You can only bind a socket once. + * You can still connect a socket (if desired) after binding. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. +**/ +- (BOOL)bindToPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Binds the UDP socket to the given port and optional interface. + * Binding should be done for server sockets that receive data prior to sending it. + * Client sockets can skip binding, + * as the OS will automatically assign the socket an available port when it starts sending data. + * + * You may optionally pass a port number of zero to immediately bind the socket, + * yet still allow the OS to automatically assign an available port. + * + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept packets from the local machine. + * + * You cannot bind a socket after its been connected. + * You can only bind a socket once. + * You can still connect a socket (if desired) after binding. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. +**/ +- (BOOL)bindToPort:(uint16_t)port interface:(NSString *)interface error:(NSError **)errPtr; + +/** + * Binds the UDP socket to the given address, specified as a sockaddr structure wrapped in a NSData object. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * Binding should be done for server sockets that receive data prior to sending it. + * Client sockets can skip binding, + * as the OS will automatically assign the socket an available port when it starts sending data. + * + * You cannot bind a socket after its been connected. + * You can only bind a socket once. + * You can still connect a socket (if desired) after binding. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. +**/ +- (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr; + +#pragma mark Connecting + +/** + * Connects the UDP socket to the given host and port. + * By design, UDP is a connectionless protocol, and connecting is not needed. + * + * Choosing to connect to a specific host/port has the following effect: + * - You will only be able to send data to the connected host/port. + * - You will only be able to receive data from the connected host/port. + * - You will receive ICMP messages that come from the connected host/port, such as "connection refused". + * + * The actual process of connecting a UDP socket does not result in any communication on the socket. + * It simply changes the internal state of the socket. + * + * You cannot bind a socket after it has been connected. + * You can only connect a socket once. + * + * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * + * This method is asynchronous as it requires a DNS lookup to resolve the given host name. + * If an obvious error is detected, this method immediately returns NO and sets errPtr. + * If you don't care about the error, you can pass nil for errPtr. + * Otherwise, this method returns YES and begins the asynchronous connection process. + * The result of the asynchronous connection process will be reported via the delegate methods. + **/ +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; + +/** + * Connects the UDP socket to the given address, specified as a sockaddr structure wrapped in a NSData object. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; + * + * By design, UDP is a connectionless protocol, and connecting is not needed. + * + * Choosing to connect to a specific address has the following effect: + * - You will only be able to send data to the connected address. + * - You will only be able to receive data from the connected address. + * - You will receive ICMP messages that come from the connected address, such as "connection refused". + * + * Connecting a UDP socket does not result in any communication on the socket. + * It simply changes the internal state of the socket. + * + * You cannot bind a socket after its been connected. + * You can only connect a socket once. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. + * + * Note: Unlike the connectToHost:onPort:error: method, this method does not require a DNS lookup. + * Thus when this method returns, the connection has either failed or fully completed. + * In other words, this method is synchronous, unlike the asynchronous connectToHost::: method. + * However, for compatibility and simplification of delegate code, if this method returns YES + * then the corresponding delegate method (udpSocket:didConnectToHost:port:) is still invoked. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +#pragma mark Multicast + +/** + * Join multicast group. + * Group should be an IP address (eg @"225.228.0.1"). + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ +- (BOOL)joinMulticastGroup:(NSString *)group error:(NSError **)errPtr; + +/** + * Join multicast group. + * Group should be an IP address (eg @"225.228.0.1"). + * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ +- (BOOL)joinMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr; + +- (BOOL)leaveMulticastGroup:(NSString *)group error:(NSError **)errPtr; +- (BOOL)leaveMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr; + +#pragma mark Reuse Port + +/** + * By default, only one socket can be bound to a given IP address + port at a time. + * To enable multiple processes to simultaneously bind to the same address+port, + * you need to enable this functionality in the socket. All processes that wish to + * use the address+port simultaneously must all enable reuse port on the socket + * bound to that port. + **/ +- (BOOL)enableReusePort:(BOOL)flag error:(NSError **)errPtr; + +#pragma mark Broadcast + +/** + * By default, the underlying socket in the OS will not allow you to send broadcast messages. + * In order to send broadcast messages, you need to enable this functionality in the socket. + * + * A broadcast is a UDP message to addresses like "192.168.255.255" or "255.255.255.255" that is + * delivered to every host on the network. + * The reason this is generally disabled by default (by the OS) is to prevent + * accidental broadcast messages from flooding the network. +**/ +- (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr; + +#pragma mark Sending + +/** + * Asynchronously sends the given data, with the given timeout and tag. + * + * This method may only be used with a connected socket. + * Recall that connecting is optional for a UDP socket. + * For connected sockets, data can only be sent to the connected address. + * For non-connected sockets, the remote destination is specified for each packet. + * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. + * + * @param data + * The data to send. + * If data is nil or zero-length, this method does nothing. + * If passing NSMutableData, please read the thread-safety notice below. + * + * @param timeout + * The timeout for the send opeartion. + * If the timeout value is negative, the send operation will not use a timeout. + * + * @param tag + * The tag is for your convenience. + * It is not sent or received over the socket in any manner what-so-ever. + * It is reported back as a parameter in the udpSocket:didSendDataWithTag: + * or udpSocket:didNotSendDataWithTag:dueToError: methods. + * You can use it as an array index, state id, type constant, etc. + * + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method + * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying + * that this particular send operation has completed. + * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. + * It simply retains it for performance reasons. + * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. + * Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Asynchronously sends the given data, with the given timeout and tag, to the given host and port. + * + * This method cannot be used with a connected socket. + * Recall that connecting is optional for a UDP socket. + * For connected sockets, data can only be sent to the connected address. + * For non-connected sockets, the remote destination is specified for each packet. + * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. + * + * @param data + * The data to send. + * If data is nil or zero-length, this method does nothing. + * If passing NSMutableData, please read the thread-safety notice below. + * + * @param host + * The destination to send the udp packet to. + * May be specified as a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). + * You may also use the convenience strings of "loopback" or "localhost". + * + * @param port + * The port of the host to send to. + * + * @param timeout + * The timeout for the send opeartion. + * If the timeout value is negative, the send operation will not use a timeout. + * + * @param tag + * The tag is for your convenience. + * It is not sent or received over the socket in any manner what-so-ever. + * It is reported back as a parameter in the udpSocket:didSendDataWithTag: + * or udpSocket:didNotSendDataWithTag:dueToError: methods. + * You can use it as an array index, state id, type constant, etc. + * + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method + * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying + * that this particular send operation has completed. + * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. + * It simply retains it for performance reasons. + * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. + * Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)sendData:(NSData *)data + toHost:(NSString *)host + port:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + tag:(long)tag; + +/** + * Asynchronously sends the given data, with the given timeout and tag, to the given address. + * + * This method cannot be used with a connected socket. + * Recall that connecting is optional for a UDP socket. + * For connected sockets, data can only be sent to the connected address. + * For non-connected sockets, the remote destination is specified for each packet. + * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. + * + * @param data + * The data to send. + * If data is nil or zero-length, this method does nothing. + * If passing NSMutableData, please read the thread-safety notice below. + * + * @param remoteAddr + * The address to send the data to (specified as a sockaddr structure wrapped in a NSData object). + * + * @param timeout + * The timeout for the send opeartion. + * If the timeout value is negative, the send operation will not use a timeout. + * + * @param tag + * The tag is for your convenience. + * It is not sent or received over the socket in any manner what-so-ever. + * It is reported back as a parameter in the udpSocket:didSendDataWithTag: + * or udpSocket:didNotSendDataWithTag:dueToError: methods. + * You can use it as an array index, state id, type constant, etc. + * + * + * Thread-Safety Note: + * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while + * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method + * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying + * that this particular send operation has completed. + * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. + * It simply retains it for performance reasons. + * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. + * Copying this data adds an unwanted/unneeded overhead. + * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket + * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time + * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. +**/ +- (void)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * You may optionally set a send filter for the socket. + * A filter can provide several interesting possibilities: + * + * 1. Optional caching of resolved addresses for domain names. + * The cache could later be consulted, resulting in fewer system calls to getaddrinfo. + * + * 2. Reusable modules of code for bandwidth monitoring. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * For more information about GCDAsyncUdpSocketSendFilterBlock, see the documentation for its typedef. + * To remove a previously set filter, invoke this method and pass a nil filterBlock and NULL filterQueue. + * + * Note: This method invokes setSendFilter:withQueue:isAsynchronous: (documented below), + * passing YES for the isAsynchronous parameter. +**/ +- (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue; + +/** + * The receive filter can be run via dispatch_async or dispatch_sync. + * Most typical situations call for asynchronous operation. + * + * However, there are a few situations in which synchronous operation is preferred. + * Such is the case when the filter is extremely minimal and fast. + * This is because dispatch_sync is faster than dispatch_async. + * + * If you choose synchronous operation, be aware of possible deadlock conditions. + * Since the socket queue is executing your block via dispatch_sync, + * then you cannot perform any tasks which may invoke dispatch_sync on the socket queue. + * For example, you can't query properties on the socket. +**/ +- (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock + withQueue:(dispatch_queue_t)filterQueue + isAsynchronous:(BOOL)isAsynchronous; + +#pragma mark Receiving + +/** + * There are two modes of operation for receiving packets: one-at-a-time & continuous. + * + * In one-at-a-time mode, you call receiveOnce everytime your delegate is ready to process an incoming udp packet. + * Receiving packets one-at-a-time may be better suited for implementing certain state machine code, + * where your state machine may not always be ready to process incoming packets. + * + * In continuous mode, the delegate is invoked immediately everytime incoming udp packets are received. + * Receiving packets continuously is better suited to real-time streaming applications. + * + * You may switch back and forth between one-at-a-time mode and continuous mode. + * If the socket is currently in continuous mode, calling this method will switch it to one-at-a-time mode. + * + * When a packet is received (and not filtered by the optional receive filter), + * the delegate method (udpSocket:didReceiveData:fromAddress:withFilterContext:) is invoked. + * + * If the socket is able to begin receiving packets, this method returns YES. + * Otherwise it returns NO, and sets the errPtr with appropriate error information. + * + * An example error: + * You created a udp socket to act as a server, and immediately called receive. + * You forgot to first bind the socket to a port number, and received a error with a message like: + * "Must bind socket before you can receive data." +**/ +- (BOOL)receiveOnce:(NSError **)errPtr; + +/** + * There are two modes of operation for receiving packets: one-at-a-time & continuous. + * + * In one-at-a-time mode, you call receiveOnce everytime your delegate is ready to process an incoming udp packet. + * Receiving packets one-at-a-time may be better suited for implementing certain state machine code, + * where your state machine may not always be ready to process incoming packets. + * + * In continuous mode, the delegate is invoked immediately everytime incoming udp packets are received. + * Receiving packets continuously is better suited to real-time streaming applications. + * + * You may switch back and forth between one-at-a-time mode and continuous mode. + * If the socket is currently in one-at-a-time mode, calling this method will switch it to continuous mode. + * + * For every received packet (not filtered by the optional receive filter), + * the delegate method (udpSocket:didReceiveData:fromAddress:withFilterContext:) is invoked. + * + * If the socket is able to begin receiving packets, this method returns YES. + * Otherwise it returns NO, and sets the errPtr with appropriate error information. + * + * An example error: + * You created a udp socket to act as a server, and immediately called receive. + * You forgot to first bind the socket to a port number, and received a error with a message like: + * "Must bind socket before you can receive data." +**/ +- (BOOL)beginReceiving:(NSError **)errPtr; + +/** + * If the socket is currently receiving (beginReceiving has been called), this method pauses the receiving. + * That is, it won't read any more packets from the underlying OS socket until beginReceiving is called again. + * + * Important Note: + * GCDAsyncUdpSocket may be running in parallel with your code. + * That is, your delegate is likely running on a separate thread/dispatch_queue. + * When you invoke this method, GCDAsyncUdpSocket may have already dispatched delegate methods to be invoked. + * Thus, if those delegate methods have already been dispatch_async'd, + * your didReceive delegate method may still be invoked after this method has been called. + * You should be aware of this, and program defensively. +**/ +- (void)pauseReceiving; + +/** + * You may optionally set a receive filter for the socket. + * This receive filter may be set to run in its own queue (independent of delegate queue). + * + * A filter can provide several useful features. + * + * 1. Many times udp packets need to be parsed. + * Since the filter can run in its own independent queue, you can parallelize this parsing quite easily. + * The end result is a parallel socket io, datagram parsing, and packet processing. + * + * 2. Many times udp packets are discarded because they are duplicate/unneeded/unsolicited. + * The filter can prevent such packets from arriving at the delegate. + * And because the filter can run in its own independent queue, this doesn't slow down the delegate. + * + * - Since the udp protocol does not guarantee delivery, udp packets may be lost. + * Many protocols built atop udp thus provide various resend/re-request algorithms. + * This sometimes results in duplicate packets arriving. + * A filter may allow you to architect the duplicate detection code to run in parallel to normal processing. + * + * - Since the udp socket may be connectionless, its possible for unsolicited packets to arrive. + * Such packets need to be ignored. + * + * 3. Sometimes traffic shapers are needed to simulate real world environments. + * A filter allows you to write custom code to simulate such environments. + * The ability to code this yourself is especially helpful when your simulated environment + * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), + * or the system tools to handle this aren't available (e.g. on a mobile device). + * + * Example: + * + * GCDAsyncUdpSocketReceiveFilterBlock filter = ^BOOL (NSData *data, NSData *address, id *context) { + * + * MyProtocolMessage *msg = [MyProtocol parseMessage:data]; + * + * *context = response; + * return (response != nil); + * }; + * [udpSocket setReceiveFilter:filter withQueue:myParsingQueue]; + * + * For more information about GCDAsyncUdpSocketReceiveFilterBlock, see the documentation for its typedef. + * To remove a previously set filter, invoke this method and pass a nil filterBlock and NULL filterQueue. + * + * Note: This method invokes setReceiveFilter:withQueue:isAsynchronous: (documented below), + * passing YES for the isAsynchronous parameter. +**/ +- (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue; + +/** + * The receive filter can be run via dispatch_async or dispatch_sync. + * Most typical situations call for asynchronous operation. + * + * However, there are a few situations in which synchronous operation is preferred. + * Such is the case when the filter is extremely minimal and fast. + * This is because dispatch_sync is faster than dispatch_async. + * + * If you choose synchronous operation, be aware of possible deadlock conditions. + * Since the socket queue is executing your block via dispatch_sync, + * then you cannot perform any tasks which may invoke dispatch_sync on the socket queue. + * For example, you can't query properties on the socket. +**/ +- (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock + withQueue:(dispatch_queue_t)filterQueue + isAsynchronous:(BOOL)isAsynchronous; + +#pragma mark Closing + +/** + * Immediately closes the underlying socket. + * Any pending send operations are discarded. + * + * The GCDAsyncUdpSocket instance may optionally be used again. + * (it will setup/configure/use another unnderlying BSD socket). +**/ +- (void)close; + +/** + * Closes the underlying socket after all pending send operations have been sent. + * + * The GCDAsyncUdpSocket instance may optionally be used again. + * (it will setup/configure/use another unnderlying BSD socket). +**/ +- (void)closeAfterSending; + +#pragma mark Advanced +/** + * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. + * In most cases, the instance creates this queue itself. + * However, to allow for maximum flexibility, the internal queue may be passed in the init method. + * This allows for some advanced options such as controlling socket priority via target queues. + * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. + * + * For example, imagine there are 2 queues: + * dispatch_queue_t socketQueue; + * dispatch_queue_t socketTargetQueue; + * + * If you do this (pseudo-code): + * socketQueue.targetQueue = socketTargetQueue; + * + * Then all socketQueue operations will actually get run on the given socketTargetQueue. + * This is fine and works great in most situations. + * But if you run code directly from within the socketTargetQueue that accesses the socket, + * you could potentially get deadlock. Imagine the following code: + * + * - (BOOL)socketHasSomething + * { + * __block BOOL result = NO; + * dispatch_block_t block = ^{ + * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; + * } + * if (is_executing_on_queue(socketQueue)) + * block(); + * else + * dispatch_sync(socketQueue, block); + * + * return result; + * } + * + * What happens if you call this method from the socketTargetQueue? The result is deadlock. + * This is because the GCD API offers no mechanism to discover a queue's targetQueue. + * Thus we have no idea if our socketQueue is configured with a targetQueue. + * If we had this information, we could easily avoid deadlock. + * But, since these API's are missing or unfeasible, you'll have to explicitly set it. + * + * IF you pass a socketQueue via the init method, + * AND you've configured the passed socketQueue with a targetQueue, + * THEN you should pass the end queue in the target hierarchy. + * + * For example, consider the following queue hierarchy: + * socketQueue -> ipQueue -> moduleQueue + * + * This example demonstrates priority shaping within some server. + * All incoming client connections from the same IP address are executed on the same target queue. + * And all connections for a particular module are executed on the same target queue. + * Thus, the priority of all networking for the entire module can be changed on the fly. + * Additionally, networking traffic from a single IP cannot monopolize the module. + * + * Here's how you would accomplish something like that: + * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock + * { + * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); + * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; + * + * dispatch_set_target_queue(socketQueue, ipQueue); + * dispatch_set_target_queue(iqQueue, moduleQueue); + * + * return socketQueue; + * } + * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket + * { + * [clientConnections addObject:newSocket]; + * [newSocket markSocketQueueTargetQueue:moduleQueue]; + * } + * + * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. + * This is often NOT the case, as such queues are used solely for execution shaping. + **/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; + +/** + * It's not thread-safe to access certain variables from outside the socket's internal queue. + * + * For example, the socket file descriptor. + * File descriptors are simply integers which reference an index in the per-process file table. + * However, when one requests a new file descriptor (by opening a file or socket), + * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. + * So if we're not careful, the following could be possible: + * + * - Thread A invokes a method which returns the socket's file descriptor. + * - The socket is closed via the socket's internal queue on thread B. + * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. + * - Thread A is now accessing/altering the file instead of the socket. + * + * In addition to this, other variables are not actually objects, + * and thus cannot be retained/released or even autoreleased. + * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. + * + * Although there are internal variables that make it difficult to maintain thread-safety, + * it is important to provide access to these variables + * to ensure this class can be used in a wide array of environments. + * This method helps to accomplish this by invoking the current block on the socket's internal queue. + * The methods below can be invoked from within the block to access + * those generally thread-unsafe internal variables in a thread-safe manner. + * The given block will be invoked synchronously on the socket's internal queue. + * + * If you save references to any protected variables and use them outside the block, you do so at your own peril. +**/ +- (void)performBlock:(dispatch_block_t)block; + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Provides access to the socket's file descriptor(s). + * If the socket isn't connected, or explicity bound to a particular interface, + * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. +**/ +- (int)socketFD; +- (int)socket4FD; +- (int)socket6FD; + +#if TARGET_OS_IPHONE + +/** + * These methods are only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Returns (creating if necessary) a CFReadStream/CFWriteStream for the internal socket. + * + * Generally GCDAsyncUdpSocket doesn't use CFStream. (It uses the faster GCD API's.) + * However, if you need one for any reason, + * these methods are a convenient way to get access to a safe instance of one. +**/ +- (CFReadStreamRef)readStream; +- (CFWriteStreamRef)writeStream; + +/** + * This method is only available from within the context of a performBlock: invocation. + * See the documentation for the performBlock: method above. + * + * Configures the socket to allow it to operate when the iOS application has been backgrounded. + * In other words, this method creates a read & write stream, and invokes: + * + * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); + * + * Returns YES if successful, NO otherwise. + * + * Example usage: + * + * [asyncUdpSocket performBlock:^{ + * [asyncUdpSocket enableBackgroundingOnSocket]; + * }]; + * + * + * NOTE : Apple doesn't currently support backgrounding UDP sockets. (Only TCP for now). +**/ +//- (BOOL)enableBackgroundingOnSockets; + +#endif + +#pragma mark Utilities + +/** + * Extracting host/port/family information from raw address data. +**/ + ++ (NSString *)hostFromAddress:(NSData *)address; ++ (uint16_t)portFromAddress:(NSData *)address; ++ (int)familyFromAddress:(NSData *)address; + ++ (BOOL)isIPv4Address:(NSData *)address; ++ (BOOL)isIPv6Address:(NSData *)address; + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address; ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(int *)afPtr fromAddress:(NSData *)address; + +@end + diff --git a/Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncUdpSocket.m b/Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncUdpSocket.m new file mode 100644 index 0000000..27c739d --- /dev/null +++ b/Example/Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncUdpSocket.m @@ -0,0 +1,5430 @@ +// +// GCDAsyncUdpSocket +// +// This class is in the public domain. +// Originally created by Robbie Hanson of Deusty LLC. +// Updated and maintained by Deusty LLC and the Apple development community. +// +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import "GCDAsyncUdpSocket.h" + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +// For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC +#endif + +#if TARGET_OS_IPHONE + #import + #import +#endif + +#import +#import +#import +#import +#import +#import +#import + + +#if 0 + +// Logging Enabled - See log level below + +// Logging uses the CocoaLumberjack framework (which is also GCD based). +// http://code.google.com/p/cocoalumberjack/ +// +// It allows us to do a lot of logging without significantly slowing down the code. +#import "DDLog.h" + +#define LogAsync NO +#define LogContext 65535 + +#define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) +#define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) + +#define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) +#define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) + +#define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) +#define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__) + +// Log levels : off, error, warn, info, verbose +static const int logLevel = LOG_LEVEL_VERBOSE; + +#else + +// Logging Disabled + +#define LogError(frmt, ...) {} +#define LogWarn(frmt, ...) {} +#define LogInfo(frmt, ...) {} +#define LogVerbose(frmt, ...) {} + +#define LogCError(frmt, ...) {} +#define LogCWarn(frmt, ...) {} +#define LogCInfo(frmt, ...) {} +#define LogCVerbose(frmt, ...) {} + +#define LogTrace() {} +#define LogCTrace(frmt, ...) {} + +#endif + +/** + * Seeing a return statements within an inner block + * can sometimes be mistaken for a return point of the enclosing method. + * This makes inline blocks a bit easier to read. +**/ +#define return_from_block return + +/** + * A socket file descriptor is really just an integer. + * It represents the index of the socket within the kernel. + * This makes invalid file descriptor comparisons easier to read. +**/ +#define SOCKET_NULL -1 + +/** + * Just to type less code. +**/ +#define AutoreleasedBlock(block) ^{ @autoreleasepool { block(); }} + + +@class GCDAsyncUdpSendPacket; + +NSString *const GCDAsyncUdpSocketException = @"GCDAsyncUdpSocketException"; +NSString *const GCDAsyncUdpSocketErrorDomain = @"GCDAsyncUdpSocketErrorDomain"; + +NSString *const GCDAsyncUdpSocketQueueName = @"GCDAsyncUdpSocket"; +NSString *const GCDAsyncUdpSocketThreadName = @"GCDAsyncUdpSocket-CFStream"; + +enum GCDAsyncUdpSocketFlags +{ + kDidCreateSockets = 1 << 0, // If set, the sockets have been created. + kDidBind = 1 << 1, // If set, bind has been called. + kConnecting = 1 << 2, // If set, a connection attempt is in progress. + kDidConnect = 1 << 3, // If set, socket is connected. + kReceiveOnce = 1 << 4, // If set, one-at-a-time receive is enabled + kReceiveContinuous = 1 << 5, // If set, continuous receive is enabled + kIPv4Deactivated = 1 << 6, // If set, socket4 was closed due to bind or connect on IPv6. + kIPv6Deactivated = 1 << 7, // If set, socket6 was closed due to bind or connect on IPv4. + kSend4SourceSuspended = 1 << 8, // If set, send4Source is suspended. + kSend6SourceSuspended = 1 << 9, // If set, send6Source is suspended. + kReceive4SourceSuspended = 1 << 10, // If set, receive4Source is suspended. + kReceive6SourceSuspended = 1 << 11, // If set, receive6Source is suspended. + kSock4CanAcceptBytes = 1 << 12, // If set, we know socket4 can accept bytes. If unset, it's unknown. + kSock6CanAcceptBytes = 1 << 13, // If set, we know socket6 can accept bytes. If unset, it's unknown. + kForbidSendReceive = 1 << 14, // If set, no new send or receive operations are allowed to be queued. + kCloseAfterSends = 1 << 15, // If set, close as soon as no more sends are queued. + kFlipFlop = 1 << 16, // Used to alternate between IPv4 and IPv6 sockets. +#if TARGET_OS_IPHONE + kAddedStreamListener = 1 << 17, // If set, CFStreams have been added to listener thread +#endif +}; + +enum GCDAsyncUdpSocketConfig +{ + kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled + kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled + kPreferIPv4 = 1 << 2, // If set, IPv4 is preferred over IPv6 + kPreferIPv6 = 1 << 3, // If set, IPv6 is preferred over IPv4 +}; + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface GCDAsyncUdpSocket () +{ +#if __has_feature(objc_arc_weak) + __weak id delegate; +#else + __unsafe_unretained id delegate; +#endif + dispatch_queue_t delegateQueue; + + GCDAsyncUdpSocketReceiveFilterBlock receiveFilterBlock; + dispatch_queue_t receiveFilterQueue; + BOOL receiveFilterAsync; + + GCDAsyncUdpSocketSendFilterBlock sendFilterBlock; + dispatch_queue_t sendFilterQueue; + BOOL sendFilterAsync; + + uint32_t flags; + uint16_t config; + + uint16_t max4ReceiveSize; + uint32_t max6ReceiveSize; + + int socket4FD; + int socket6FD; + + dispatch_queue_t socketQueue; + + dispatch_source_t send4Source; + dispatch_source_t send6Source; + dispatch_source_t receive4Source; + dispatch_source_t receive6Source; + dispatch_source_t sendTimer; + + GCDAsyncUdpSendPacket *currentSend; + NSMutableArray *sendQueue; + + unsigned long socket4FDBytesAvailable; + unsigned long socket6FDBytesAvailable; + + uint32_t pendingFilterOperations; + + NSData *cachedLocalAddress4; + NSString *cachedLocalHost4; + uint16_t cachedLocalPort4; + + NSData *cachedLocalAddress6; + NSString *cachedLocalHost6; + uint16_t cachedLocalPort6; + + NSData *cachedConnectedAddress; + NSString *cachedConnectedHost; + uint16_t cachedConnectedPort; + int cachedConnectedFamily; + + void *IsOnSocketQueueOrTargetQueueKey; + +#if TARGET_OS_IPHONE + CFStreamClientContext streamContext; + CFReadStreamRef readStream4; + CFReadStreamRef readStream6; + CFWriteStreamRef writeStream4; + CFWriteStreamRef writeStream6; +#endif + + id userData; +} + +- (void)resumeSend4Source; +- (void)resumeSend6Source; +- (void)resumeReceive4Source; +- (void)resumeReceive6Source; +- (void)closeSockets; + +- (void)maybeConnect; +- (BOOL)connectWithAddress4:(NSData *)address4 error:(NSError **)errPtr; +- (BOOL)connectWithAddress6:(NSData *)address6 error:(NSError **)errPtr; + +- (void)maybeDequeueSend; +- (void)doPreSend; +- (void)doSend; +- (void)endCurrentSend; +- (void)setupSendTimerWithTimeout:(NSTimeInterval)timeout; + +- (void)doReceive; +- (void)doReceiveEOF; + +- (void)closeWithError:(NSError *)error; + +- (BOOL)performMulticastRequest:(int)requestType forGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr; + +#if TARGET_OS_IPHONE +- (BOOL)createReadAndWriteStreams:(NSError **)errPtr; +- (BOOL)registerForStreamCallbacks:(NSError **)errPtr; +- (BOOL)addStreamsToRunLoop:(NSError **)errPtr; +- (BOOL)openStreams:(NSError **)errPtr; +- (void)removeStreamsFromRunLoop; +- (void)closeReadAndWriteStreams; +#endif + ++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4; ++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6; ++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4; ++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6; + +#if TARGET_OS_IPHONE +// Forward declaration ++ (void)listenerThread; +#endif + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The GCDAsyncUdpSendPacket encompasses the instructions for a single send/write. +**/ +@interface GCDAsyncUdpSendPacket : NSObject { +@public + NSData *buffer; + NSTimeInterval timeout; + long tag; + + BOOL resolveInProgress; + BOOL filterInProgress; + + NSArray *resolvedAddresses; + NSError *resolveError; + + NSData *address; + int addressFamily; +} + +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i; + +@end + +@implementation GCDAsyncUdpSendPacket + +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i +{ + if ((self = [super init])) + { + buffer = d; + timeout = t; + tag = i; + + resolveInProgress = NO; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface GCDAsyncUdpSpecialPacket : NSObject { +@public +// uint8_t type; + + BOOL resolveInProgress; + + NSArray *addresses; + NSError *error; +} + +- (id)init; + +@end + +@implementation GCDAsyncUdpSpecialPacket + +- (id)init +{ + self = [super init]; + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation GCDAsyncUdpSocket + +- (id)init +{ + LogTrace(); + + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; +} + +- (id)initWithSocketQueue:(dispatch_queue_t)sq +{ + LogTrace(); + + return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; +} + +- (id)initWithDelegate:(id )aDelegate delegateQueue:(dispatch_queue_t)dq +{ + LogTrace(); + + return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; +} + +- (id)initWithDelegate:(id )aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq +{ + LogTrace(); + + if ((self = [super init])) + { + delegate = aDelegate; + + if (dq) + { + delegateQueue = dq; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(delegateQueue); + #endif + } + + max4ReceiveSize = 9216; + max6ReceiveSize = 9216; + + socket4FD = SOCKET_NULL; + socket6FD = SOCKET_NULL; + + if (sq) + { + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), + @"The given socketQueue parameter must not be a concurrent queue."); + + socketQueue = sq; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(socketQueue); + #endif + } + else + { + socketQueue = dispatch_queue_create([GCDAsyncUdpSocketQueueName UTF8String], NULL); + } + + // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. + // From the documentation: + // + // > Keys are only compared as pointers and are never dereferenced. + // > Thus, you can use a pointer to a static variable for a specific subsystem or + // > any other value that allows you to identify the value uniquely. + // + // We're just going to use the memory address of an ivar. + // Specifically an ivar that is explicitly named for our purpose to make the code more readable. + // + // However, it feels tedious (and less readable) to include the "&" all the time: + // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) + // + // So we're going to make it so it doesn't matter if we use the '&' or not, + // by assigning the value of the ivar to the address of the ivar. + // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; + + IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; + + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); + + currentSend = nil; + sendQueue = [[NSMutableArray alloc] initWithCapacity:5]; + + #if TARGET_OS_IPHONE + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationWillEnterForeground:) + name:UIApplicationWillEnterForegroundNotification + object:nil]; + #endif + } + return self; +} + +- (void)dealloc +{ + LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); + +#if TARGET_OS_IPHONE + [[NSNotificationCenter defaultCenter] removeObserver:self]; +#endif + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + [self closeWithError:nil]; + } + else + { + dispatch_sync(socketQueue, ^{ + [self closeWithError:nil]; + }); + } + + delegate = nil; + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + #endif + delegateQueue = NULL; + + #if !OS_OBJECT_USE_OBJC + if (socketQueue) dispatch_release(socketQueue); + #endif + socketQueue = NULL; + + LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)delegate +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegate; + } + else + { + __block id result = nil; + + dispatch_sync(socketQueue, ^{ + result = delegate; + }); + + return result; + } +} + +- (void)setDelegate:(id )newDelegate synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + delegate = newDelegate; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id )newDelegate +{ + [self setDelegate:newDelegate synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id )newDelegate +{ + [self setDelegate:newDelegate synchronously:YES]; +} + +- (dispatch_queue_t)delegateQueue +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + return delegateQueue; + } + else + { + __block dispatch_queue_t result = NULL; + + dispatch_sync(socketQueue, ^{ + result = delegateQueue; + }); + + return result; + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegateQueue:newDelegateQueue synchronously:YES]; +} + +- (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + if (delegatePtr) *delegatePtr = delegate; + if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; + } + else + { + __block id dPtr = NULL; + __block dispatch_queue_t dqPtr = NULL; + + dispatch_sync(socketQueue, ^{ + dPtr = delegate; + dqPtr = delegateQueue; + }); + + if (delegatePtr) *delegatePtr = dPtr; + if (delegateQueuePtr) *delegateQueuePtr = dqPtr; + } +} + +- (void)setDelegate:(id )newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously +{ + dispatch_block_t block = ^{ + + delegate = newDelegate; + + #if !OS_OBJECT_USE_OBJC + if (delegateQueue) dispatch_release(delegateQueue); + if (newDelegateQueue) dispatch_retain(newDelegateQueue); + #endif + + delegateQueue = newDelegateQueue; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { + block(); + } + else { + if (synchronously) + dispatch_sync(socketQueue, block); + else + dispatch_async(socketQueue, block); + } +} + +- (void)setDelegate:(id )newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; +} + +- (void)synchronouslySetDelegate:(id )newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue +{ + [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; +} + +- (BOOL)isIPv4Enabled +{ + // Note: YES means kIPv4Disabled is OFF + + __block BOOL result = NO; + + dispatch_block_t block = ^{ + + result = ((config & kIPv4Disabled) == 0); + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setIPv4Enabled:(BOOL)flag +{ + // Note: YES means kIPv4Disabled is OFF + + dispatch_block_t block = ^{ + + LogVerbose(@"%@ %@", THIS_METHOD, (flag ? @"YES" : @"NO")); + + if (flag) + config &= ~kIPv4Disabled; + else + config |= kIPv4Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv6Enabled +{ + // Note: YES means kIPv6Disabled is OFF + + __block BOOL result = NO; + + dispatch_block_t block = ^{ + + result = ((config & kIPv6Disabled) == 0); + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setIPv6Enabled:(BOOL)flag +{ + // Note: YES means kIPv6Disabled is OFF + + dispatch_block_t block = ^{ + + LogVerbose(@"%@ %@", THIS_METHOD, (flag ? @"YES" : @"NO")); + + if (flag) + config &= ~kIPv6Disabled; + else + config |= kIPv6Disabled; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (BOOL)isIPv4Preferred +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (config & kPreferIPv4) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPv6Preferred +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (config & kPreferIPv6) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPVersionNeutral +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (config & (kPreferIPv4 | kPreferIPv6)) == 0; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setPreferIPv4 +{ + dispatch_block_t block = ^{ + + LogTrace(); + + config |= kPreferIPv4; + config &= ~kPreferIPv6; + + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (void)setPreferIPv6 +{ + dispatch_block_t block = ^{ + + LogTrace(); + + config &= ~kPreferIPv4; + config |= kPreferIPv6; + + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (void)setIPVersionNeutral +{ + dispatch_block_t block = ^{ + + LogTrace(); + + config &= ~kPreferIPv4; + config &= ~kPreferIPv6; + + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (uint16_t)maxReceiveIPv4BufferSize +{ + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + result = max4ReceiveSize; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setMaxReceiveIPv4BufferSize:(uint16_t)max +{ + dispatch_block_t block = ^{ + + LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); + + max4ReceiveSize = max; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (uint32_t)maxReceiveIPv6BufferSize +{ + __block uint32_t result = 0; + + dispatch_block_t block = ^{ + + result = max6ReceiveSize; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setMaxReceiveIPv6BufferSize:(uint32_t)max +{ + dispatch_block_t block = ^{ + + LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); + + max6ReceiveSize = max; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + + +- (id)userData +{ + __block id result = nil; + + dispatch_block_t block = ^{ + + result = userData; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (void)setUserData:(id)arbitraryUserData +{ + dispatch_block_t block = ^{ + + if (userData != arbitraryUserData) + { + userData = arbitraryUserData; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Delegate Helpers +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)notifyDidConnectToAddress:(NSData *)anAddress +{ + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didConnectToAddress:)]) + { + NSData *address = [anAddress copy]; // In case param is NSMutableData + + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocket:self didConnectToAddress:address]; + }}); + } +} + +- (void)notifyDidNotConnect:(NSError *)error +{ + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didNotConnect:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocket:self didNotConnect:error]; + }}); + } +} + +- (void)notifyDidSendDataWithTag:(long)tag +{ + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didSendDataWithTag:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocket:self didSendDataWithTag:tag]; + }}); + } +} + +- (void)notifyDidNotSendDataWithTag:(long)tag dueToError:(NSError *)error +{ + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didNotSendDataWithTag:dueToError:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocket:self didNotSendDataWithTag:tag dueToError:error]; + }}); + } +} + +- (void)notifyDidReceiveData:(NSData *)data fromAddress:(NSData *)address withFilterContext:(id)context +{ + LogTrace(); + + SEL selector = @selector(udpSocket:didReceiveData:fromAddress:withFilterContext:); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:selector]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocket:self didReceiveData:data fromAddress:address withFilterContext:context]; + }}); + } +} + +- (void)notifyDidCloseWithError:(NSError *)error +{ + LogTrace(); + + __strong id theDelegate = delegate; + if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocketDidClose:withError:)]) + { + dispatch_async(delegateQueue, ^{ @autoreleasepool { + + [theDelegate udpSocketDidClose:self withError:error]; + }}); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSError *)badConfigError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain + code:GCDAsyncUdpSocketBadConfigError + userInfo:userInfo]; +} + +- (NSError *)badParamError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain + code:GCDAsyncUdpSocketBadParamError + userInfo:userInfo]; +} + +- (NSError *)gaiError:(int)gai_error +{ + NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; +} + +- (NSError *)errnoErrorWithReason:(NSString *)reason +{ + NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo; + + if (reason) + userInfo = [NSDictionary dictionaryWithObjectsAndKeys:errMsg, NSLocalizedDescriptionKey, + reason, NSLocalizedFailureReasonErrorKey, nil]; + else + userInfo = [NSDictionary dictionaryWithObjectsAndKeys:errMsg, NSLocalizedDescriptionKey, nil]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +- (NSError *)errnoError +{ + return [self errnoErrorWithReason:nil]; +} + +/** + * Returns a standard send timeout error. +**/ +- (NSError *)sendTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncUdpSocketSendTimeoutError", + @"GCDAsyncUdpSocket", [NSBundle mainBundle], + @"Send operation timed out", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain + code:GCDAsyncUdpSocketSendTimeoutError + userInfo:userInfo]; +} + +- (NSError *)socketClosedError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncUdpSocketClosedError", + @"GCDAsyncUdpSocket", [NSBundle mainBundle], + @"Socket closed", nil); + + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketClosedError userInfo:userInfo]; +} + +- (NSError *)otherError:(NSString *)errMsg +{ + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain + code:GCDAsyncUdpSocketOtherError + userInfo:userInfo]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)preOp:(NSError **)errPtr +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (delegate == nil) // Must have delegate set + { + if (errPtr) + { + NSString *msg = @"Attempting to use socket without a delegate. Set a delegate first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if (delegateQueue == NULL) // Must have delegate queue set + { + if (errPtr) + { + NSString *msg = @"Attempting to use socket without a delegate queue. Set a delegate queue first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + return YES; +} + +/** + * This method executes on a global concurrent queue. + * When complete, it executes the given completion block on the socketQueue. +**/ +- (void)asyncResolveHost:(NSString *)aHost + port:(uint16_t)port + withCompletionBlock:(void (^)(NSArray *addresses, NSError *error))completionBlock +{ + LogTrace(); + + // Check parameter(s) + + if (aHost == nil) + { + NSString *msg = @"The host param is nil. Should be domain name or IP address string."; + NSError *error = [self badParamError:msg]; + + // We should still use dispatch_async since this method is expected to be asynchronous + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + completionBlock(nil, error); + }}); + + return; + } + + // It's possible that the given aHost parameter is actually a NSMutableString. + // So we want to copy it now, within this block that will be executed synchronously. + // This way the asynchronous lookup block below doesn't have to worry about it changing. + + NSString *host = [aHost copy]; + + + dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { + + NSMutableArray *addresses = [NSMutableArray arrayWithCapacity:2]; + NSError *error = nil; + + if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) + { + // Use LOOPBACK address + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(struct sockaddr_in); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(struct sockaddr_in6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_loopback; + + // Wrap the native address structures and add to list + [addresses addObject:[NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]]; + [addresses addObject:[NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]]; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + + int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); + + if (gai_error) + { + error = [self gaiError:gai_error]; + } + else + { + for(res = res0; res; res = res->ai_next) + { + if (res->ai_family == AF_INET) + { + // Found IPv4 address + // Wrap the native address structure and add to list + + [addresses addObject:[NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]]; + } + else if (res->ai_family == AF_INET6) + { + // Found IPv6 address + // Wrap the native address structure and add to list + + [addresses addObject:[NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]]; + } + } + freeaddrinfo(res0); + + if ([addresses count] == 0) + { + error = [self gaiError:EAI_FAIL]; + } + } + } + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + completionBlock(addresses, error); + }}); + + }}); +} + +/** + * This method picks an address from the given list of addresses. + * The address picked depends upon which protocols are disabled, deactived, & preferred. + * + * Returns the address family (AF_INET or AF_INET6) of the picked address, + * or AF_UNSPEC and the corresponding error is there's a problem. +**/ +- (int)getAddress:(NSData **)addressPtr error:(NSError **)errorPtr fromAddresses:(NSArray *)addresses +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert([addresses count] > 0, @"Expected at least one address"); + + int resultAF = AF_UNSPEC; + NSData *resultAddress = nil; + NSError *resultError = nil; + + // Check for problems + + BOOL resolvedIPv4Address = NO; + BOOL resolvedIPv6Address = NO; + + for (NSData *address in addresses) + { + switch ([[self class] familyFromAddress:address]) + { + case AF_INET : resolvedIPv4Address = YES; break; + case AF_INET6 : resolvedIPv6Address = YES; break; + + default : NSAssert(NO, @"Addresses array contains invalid address"); + } + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && !resolvedIPv6Address) + { + NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address(es)."; + resultError = [self otherError:msg]; + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; + } + + if (isIPv6Disabled && !resolvedIPv4Address) + { + NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address(es)."; + resultError = [self otherError:msg]; + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; + } + + BOOL isIPv4Deactivated = (flags & kIPv4Deactivated) ? YES : NO; + BOOL isIPv6Deactivated = (flags & kIPv6Deactivated) ? YES : NO; + + if (isIPv4Deactivated && !resolvedIPv6Address) + { + NSString *msg = @"IPv4 has been deactivated due to bind/connect, and DNS lookup found no IPv6 address(es)."; + resultError = [self otherError:msg]; + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; + } + + if (isIPv6Deactivated && !resolvedIPv4Address) + { + NSString *msg = @"IPv6 has been deactivated due to bind/connect, and DNS lookup found no IPv4 address(es)."; + resultError = [self otherError:msg]; + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; + } + + // Extract first IPv4 and IPv6 address in list + + BOOL ipv4WasFirstInList = YES; + NSData *address4 = nil; + NSData *address6 = nil; + + for (NSData *address in addresses) + { + int af = [[self class] familyFromAddress:address]; + + if (af == AF_INET) + { + if (address4 == nil) + { + address4 = address; + + if (address6) + break; + else + ipv4WasFirstInList = YES; + } + } + else // af == AF_INET6 + { + if (address6 == nil) + { + address6 = address; + + if (address4) + break; + else + ipv4WasFirstInList = NO; + } + } + } + + // Determine socket type + + BOOL preferIPv4 = (config & kPreferIPv4) ? YES : NO; + BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; + + BOOL useIPv4 = ((preferIPv4 && address4) || (address6 == nil)); + BOOL useIPv6 = ((preferIPv6 && address6) || (address4 == nil)); + + NSAssert(!(preferIPv4 && preferIPv6), @"Invalid config state"); + NSAssert(!(useIPv4 && useIPv6), @"Invalid logic"); + + if (useIPv4 || (!useIPv6 && ipv4WasFirstInList)) + { + resultAF = AF_INET; + resultAddress = address4; + } + else + { + resultAF = AF_INET6; + resultAddress = address6; + } + + if (addressPtr) *addressPtr = resultAddress; + if (errorPtr) *errorPtr = resultError; + + return resultAF; +} + +/** + * Finds the address(es) of an interface description. + * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). +**/ +- (void)convertIntefaceDescription:(NSString *)interfaceDescription + port:(uint16_t)port + intoAddress4:(NSData **)interfaceAddr4Ptr + address6:(NSData **)interfaceAddr6Ptr +{ + NSData *addr4 = nil; + NSData *addr6 = nil; + + if (interfaceDescription == nil) + { + // ANY address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(sockaddr4); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(sockaddr6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_any; + + addr4 = [NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else if ([interfaceDescription isEqualToString:@"localhost"] || + [interfaceDescription isEqualToString:@"loopback"]) + { + // LOOPBACK address + + struct sockaddr_in sockaddr4; + memset(&sockaddr4, 0, sizeof(sockaddr4)); + + sockaddr4.sin_len = sizeof(struct sockaddr_in); + sockaddr4.sin_family = AF_INET; + sockaddr4.sin_port = htons(port); + sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + + struct sockaddr_in6 sockaddr6; + memset(&sockaddr6, 0, sizeof(sockaddr6)); + + sockaddr6.sin6_len = sizeof(struct sockaddr_in6); + sockaddr6.sin6_family = AF_INET6; + sockaddr6.sin6_port = htons(port); + sockaddr6.sin6_addr = in6addr_loopback; + + addr4 = [NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; + addr6 = [NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; + } + else + { + const char *iface = [interfaceDescription UTF8String]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) + { + // IPv4 + + struct sockaddr_in *addr = (struct sockaddr_in *)cursor->ifa_addr; + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + struct sockaddr_in nativeAddr4 = *addr; + nativeAddr4.sin_port = htons(port); + + addr4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + else + { + char ip[INET_ADDRSTRLEN]; + + const char *conversion; + conversion = inet_ntop(AF_INET, &addr->sin_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + struct sockaddr_in nativeAddr4 = *addr; + nativeAddr4.sin_port = htons(port); + + addr4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + } + } + } + else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) + { + // IPv6 + + struct sockaddr_in6 *addr = (struct sockaddr_in6 *)cursor->ifa_addr; + + if (strcmp(cursor->ifa_name, iface) == 0) + { + // Name match + + struct sockaddr_in6 nativeAddr6 = *addr; + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else + { + char ip[INET6_ADDRSTRLEN]; + + const char *conversion; + conversion = inet_ntop(AF_INET6, &addr->sin6_addr, ip, sizeof(ip)); + + if ((conversion != NULL) && (strcmp(ip, iface) == 0)) + { + // IP match + + struct sockaddr_in6 nativeAddr6 = *addr; + nativeAddr6.sin6_port = htons(port); + + addr6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + } + + if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; + if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; +} + +/** + * Converts a numeric hostname into its corresponding address. + * The hostname is expected to be an IPv4 or IPv6 address represented as a human-readable string. (e.g. 192.168.4.34) +**/ +- (void)convertNumericHost:(NSString *)numericHost + port:(uint16_t)port + intoAddress4:(NSData **)addr4Ptr + address6:(NSData **)addr6Ptr +{ + NSData *addr4 = nil; + NSData *addr6 = nil; + + if (numericHost) + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + hints.ai_flags = AI_NUMERICHOST; // No name resolution should be attempted + + if (getaddrinfo([numericHost UTF8String], [portStr UTF8String], &hints, &res0) == 0) + { + for (res = res0; res; res = res->ai_next) + { + if ((addr4 == nil) && (res->ai_family == AF_INET)) + { + // Found IPv4 address + // Wrap the native address structure + addr4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + else if ((addr6 == nil) && (res->ai_family == AF_INET6)) + { + // Found IPv6 address + // Wrap the native address structure + addr6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + } + freeaddrinfo(res0); + } + } + + if (addr4Ptr) *addr4Ptr = addr4; + if (addr6Ptr) *addr6Ptr = addr6; +} + +- (BOOL)isConnectedToAddress4:(NSData *)someAddr4 +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(flags & kDidConnect, @"Not connected"); + NSAssert(cachedConnectedAddress, @"Expected cached connected address"); + + if (cachedConnectedFamily != AF_INET) + { + return NO; + } + + const struct sockaddr_in *sSockaddr4 = (struct sockaddr_in *)[someAddr4 bytes]; + const struct sockaddr_in *cSockaddr4 = (struct sockaddr_in *)[cachedConnectedAddress bytes]; + + if (memcmp(&sSockaddr4->sin_addr, &cSockaddr4->sin_addr, sizeof(struct in_addr)) != 0) + { + return NO; + } + if (memcmp(&sSockaddr4->sin_port, &cSockaddr4->sin_port, sizeof(in_port_t)) != 0) + { + return NO; + } + + return YES; +} + +- (BOOL)isConnectedToAddress6:(NSData *)someAddr6 +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(flags & kDidConnect, @"Not connected"); + NSAssert(cachedConnectedAddress, @"Expected cached connected address"); + + if (cachedConnectedFamily != AF_INET6) + { + return NO; + } + + const struct sockaddr_in6 *sSockaddr6 = (struct sockaddr_in6 *)[someAddr6 bytes]; + const struct sockaddr_in6 *cSockaddr6 = (struct sockaddr_in6 *)[cachedConnectedAddress bytes]; + + if (memcmp(&sSockaddr6->sin6_addr, &cSockaddr6->sin6_addr, sizeof(struct in6_addr)) != 0) + { + return NO; + } + if (memcmp(&sSockaddr6->sin6_port, &cSockaddr6->sin6_port, sizeof(in_port_t)) != 0) + { + return NO; + } + + return YES; +} + +- (unsigned int)indexOfInterfaceAddr4:(NSData *)interfaceAddr4 +{ + if (interfaceAddr4 == nil) + return 0; + if ([interfaceAddr4 length] != sizeof(struct sockaddr_in)) + return 0; + + int result = 0; + struct sockaddr_in *ifaceAddr = (struct sockaddr_in *)[interfaceAddr4 bytes]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if (cursor->ifa_addr->sa_family == AF_INET) + { + // IPv4 + + struct sockaddr_in *addr = (struct sockaddr_in *)cursor->ifa_addr; + + if (memcmp(&addr->sin_addr, &ifaceAddr->sin_addr, sizeof(struct in_addr)) == 0) + { + result = if_nametoindex(cursor->ifa_name); + break; + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + + return result; +} + +- (unsigned int)indexOfInterfaceAddr6:(NSData *)interfaceAddr6 +{ + if (interfaceAddr6 == nil) + return 0; + if ([interfaceAddr6 length] != sizeof(struct sockaddr_in6)) + return 0; + + int result = 0; + struct sockaddr_in6 *ifaceAddr = (struct sockaddr_in6 *)[interfaceAddr6 bytes]; + + struct ifaddrs *addrs; + const struct ifaddrs *cursor; + + if ((getifaddrs(&addrs) == 0)) + { + cursor = addrs; + while (cursor != NULL) + { + if (cursor->ifa_addr->sa_family == AF_INET6) + { + // IPv6 + + struct sockaddr_in6 *addr = (struct sockaddr_in6 *)cursor->ifa_addr; + + if (memcmp(&addr->sin6_addr, &ifaceAddr->sin6_addr, sizeof(struct in6_addr)) == 0) + { + result = if_nametoindex(cursor->ifa_name); + break; + } + } + + cursor = cursor->ifa_next; + } + + freeifaddrs(addrs); + } + + return result; +} + +- (void)setupSendAndReceiveSourcesForSocket4 +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + send4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socket4FD, 0, socketQueue); + receive4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue); + + // Setup event handlers + + dispatch_source_set_event_handler(send4Source, ^{ @autoreleasepool { + + LogVerbose(@"send4EventBlock"); + LogVerbose(@"dispatch_source_get_data(send4Source) = %lu", dispatch_source_get_data(send4Source)); + + flags |= kSock4CanAcceptBytes; + + // If we're ready to send data, do so immediately. + // Otherwise pause the send source or it will continue to fire over and over again. + + if (currentSend == nil) + { + LogVerbose(@"Nothing to send"); + [self suspendSend4Source]; + } + else if (currentSend->resolveInProgress) + { + LogVerbose(@"currentSend - waiting for address resolve"); + [self suspendSend4Source]; + } + else if (currentSend->filterInProgress) + { + LogVerbose(@"currentSend - waiting on sendFilter"); + [self suspendSend4Source]; + } + else + { + [self doSend]; + } + + }}); + + dispatch_source_set_event_handler(receive4Source, ^{ @autoreleasepool { + + LogVerbose(@"receive4EventBlock"); + + socket4FDBytesAvailable = dispatch_source_get_data(receive4Source); + LogVerbose(@"socket4FDBytesAvailable: %lu", socket4FDBytesAvailable); + + if (socket4FDBytesAvailable > 0) + [self doReceive]; + else + [self doReceiveEOF]; + + }}); + + // Setup cancel handlers + + __block int socketFDRefCount = 2; + + int theSocketFD = socket4FD; + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theSendSource = send4Source; + dispatch_source_t theReceiveSource = receive4Source; + #endif + + dispatch_source_set_cancel_handler(send4Source, ^{ + + LogVerbose(@"send4CancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(send4Source)"); + dispatch_release(theSendSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socket4FD)"); + close(theSocketFD); + } + }); + + dispatch_source_set_cancel_handler(receive4Source, ^{ + + LogVerbose(@"receive4CancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(receive4Source)"); + dispatch_release(theReceiveSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socket4FD)"); + close(theSocketFD); + } + }); + + // We will not be able to receive until the socket is bound to a port, + // either explicitly via bind, or implicitly by connect or by sending data. + // + // But we should be able to send immediately. + + socket4FDBytesAvailable = 0; + flags |= kSock4CanAcceptBytes; + + flags |= kSend4SourceSuspended; + flags |= kReceive4SourceSuspended; +} + +- (void)setupSendAndReceiveSourcesForSocket6 +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + send6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socket6FD, 0, socketQueue); + receive6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue); + + // Setup event handlers + + dispatch_source_set_event_handler(send6Source, ^{ @autoreleasepool { + + LogVerbose(@"send6EventBlock"); + LogVerbose(@"dispatch_source_get_data(send6Source) = %lu", dispatch_source_get_data(send6Source)); + + flags |= kSock6CanAcceptBytes; + + // If we're ready to send data, do so immediately. + // Otherwise pause the send source or it will continue to fire over and over again. + + if (currentSend == nil) + { + LogVerbose(@"Nothing to send"); + [self suspendSend6Source]; + } + else if (currentSend->resolveInProgress) + { + LogVerbose(@"currentSend - waiting for address resolve"); + [self suspendSend6Source]; + } + else if (currentSend->filterInProgress) + { + LogVerbose(@"currentSend - waiting on sendFilter"); + [self suspendSend6Source]; + } + else + { + [self doSend]; + } + + }}); + + dispatch_source_set_event_handler(receive6Source, ^{ @autoreleasepool { + + LogVerbose(@"receive6EventBlock"); + + socket6FDBytesAvailable = dispatch_source_get_data(receive6Source); + LogVerbose(@"socket6FDBytesAvailable: %lu", socket6FDBytesAvailable); + + if (socket6FDBytesAvailable > 0) + [self doReceive]; + else + [self doReceiveEOF]; + + }}); + + // Setup cancel handlers + + __block int socketFDRefCount = 2; + + int theSocketFD = socket6FD; + + #if !OS_OBJECT_USE_OBJC + dispatch_source_t theSendSource = send6Source; + dispatch_source_t theReceiveSource = receive6Source; + #endif + + dispatch_source_set_cancel_handler(send6Source, ^{ + + LogVerbose(@"send6CancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(send6Source)"); + dispatch_release(theSendSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socket6FD)"); + close(theSocketFD); + } + }); + + dispatch_source_set_cancel_handler(receive6Source, ^{ + + LogVerbose(@"receive6CancelBlock"); + + #if !OS_OBJECT_USE_OBJC + LogVerbose(@"dispatch_release(receive6Source)"); + dispatch_release(theReceiveSource); + #endif + + if (--socketFDRefCount == 0) + { + LogVerbose(@"close(socket6FD)"); + close(theSocketFD); + } + }); + + // We will not be able to receive until the socket is bound to a port, + // either explicitly via bind, or implicitly by connect or by sending data. + // + // But we should be able to send immediately. + + socket6FDBytesAvailable = 0; + flags |= kSock6CanAcceptBytes; + + flags |= kSend6SourceSuspended; + flags |= kReceive6SourceSuspended; +} + +- (BOOL)createSocket4:(BOOL)useIPv4 socket6:(BOOL)useIPv6 error:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(((flags & kDidCreateSockets) == 0), @"Sockets have already been created"); + + // CreateSocket Block + // This block will be invoked below. + + int(^createSocket)(int) = ^int (int domain) { + + int socketFD = socket(domain, SOCK_DGRAM, 0); + + if (socketFD == SOCKET_NULL) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; + + return SOCKET_NULL; + } + + int status; + + // Set socket options + + status = fcntl(socketFD, F_SETFL, O_NONBLOCK); + if (status == -1) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error enabling non-blocking IO on socket (fcntl)"]; + + close(socketFD); + return SOCKET_NULL; + } + + int reuseaddr = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(reuseaddr)); + if (status == -1) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error enabling address reuse (setsockopt)"]; + + close(socketFD); + return SOCKET_NULL; + } + + int nosigpipe = 1; + status = setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); + if (status == -1) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error disabling sigpipe (setsockopt)"]; + + close(socketFD); + return SOCKET_NULL; + } + + return socketFD; + }; + + // Create sockets depending upon given configuration. + + if (useIPv4) + { + LogVerbose(@"Creating IPv4 socket"); + + socket4FD = createSocket(AF_INET); + if (socket4FD == SOCKET_NULL) + { + // errPtr set in local createSocket() block + return NO; + } + } + + if (useIPv6) + { + LogVerbose(@"Creating IPv6 socket"); + + socket6FD = createSocket(AF_INET6); + if (socket6FD == SOCKET_NULL) + { + // errPtr set in local createSocket() block + + if (socket4FD != SOCKET_NULL) + { + close(socket4FD); + socket4FD = SOCKET_NULL; + } + + return NO; + } + } + + // Setup send and receive sources + + if (useIPv4) + [self setupSendAndReceiveSourcesForSocket4]; + if (useIPv6) + [self setupSendAndReceiveSourcesForSocket6]; + + flags |= kDidCreateSockets; + return YES; +} + +- (BOOL)createSockets:(NSError **)errPtr +{ + LogTrace(); + + BOOL useIPv4 = [self isIPv4Enabled]; + BOOL useIPv6 = [self isIPv6Enabled]; + + return [self createSocket4:useIPv4 socket6:useIPv6 error:errPtr]; +} + +- (void)suspendSend4Source +{ + if (send4Source && !(flags & kSend4SourceSuspended)) + { + LogVerbose(@"dispatch_suspend(send4Source)"); + + dispatch_suspend(send4Source); + flags |= kSend4SourceSuspended; + } +} + +- (void)suspendSend6Source +{ + if (send6Source && !(flags & kSend6SourceSuspended)) + { + LogVerbose(@"dispatch_suspend(send6Source)"); + + dispatch_suspend(send6Source); + flags |= kSend6SourceSuspended; + } +} + +- (void)resumeSend4Source +{ + if (send4Source && (flags & kSend4SourceSuspended)) + { + LogVerbose(@"dispatch_resume(send4Source)"); + + dispatch_resume(send4Source); + flags &= ~kSend4SourceSuspended; + } +} + +- (void)resumeSend6Source +{ + if (send6Source && (flags & kSend6SourceSuspended)) + { + LogVerbose(@"dispatch_resume(send6Source)"); + + dispatch_resume(send6Source); + flags &= ~kSend6SourceSuspended; + } +} + +- (void)suspendReceive4Source +{ + if (receive4Source && !(flags & kReceive4SourceSuspended)) + { + LogVerbose(@"dispatch_suspend(receive4Source)"); + + dispatch_suspend(receive4Source); + flags |= kReceive4SourceSuspended; + } +} + +- (void)suspendReceive6Source +{ + if (receive6Source && !(flags & kReceive6SourceSuspended)) + { + LogVerbose(@"dispatch_suspend(receive6Source)"); + + dispatch_suspend(receive6Source); + flags |= kReceive6SourceSuspended; + } +} + +- (void)resumeReceive4Source +{ + if (receive4Source && (flags & kReceive4SourceSuspended)) + { + LogVerbose(@"dispatch_resume(receive4Source)"); + + dispatch_resume(receive4Source); + flags &= ~kReceive4SourceSuspended; + } +} + +- (void)resumeReceive6Source +{ + if (receive6Source && (flags & kReceive6SourceSuspended)) + { + LogVerbose(@"dispatch_resume(receive6Source)"); + + dispatch_resume(receive6Source); + flags &= ~kReceive6SourceSuspended; + } +} + +- (void)closeSocket4 +{ + if (socket4FD != SOCKET_NULL) + { + LogVerbose(@"dispatch_source_cancel(send4Source)"); + dispatch_source_cancel(send4Source); + + LogVerbose(@"dispatch_source_cancel(receive4Source)"); + dispatch_source_cancel(receive4Source); + + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + // So we have to unpause the source if needed. + // This allows the cancel handler to be run, which in turn releases the source and closes the socket. + + [self resumeSend4Source]; + [self resumeReceive4Source]; + + // The sockets will be closed by the cancel handlers of the corresponding source + + send4Source = NULL; + receive4Source = NULL; + + socket4FD = SOCKET_NULL; + + // Clear socket states + + socket4FDBytesAvailable = 0; + flags &= ~kSock4CanAcceptBytes; + + // Clear cached info + + cachedLocalAddress4 = nil; + cachedLocalHost4 = nil; + cachedLocalPort4 = 0; + } +} + +- (void)closeSocket6 +{ + if (socket6FD != SOCKET_NULL) + { + LogVerbose(@"dispatch_source_cancel(send6Source)"); + dispatch_source_cancel(send6Source); + + LogVerbose(@"dispatch_source_cancel(receive6Source)"); + dispatch_source_cancel(receive6Source); + + // For some crazy reason (in my opinion), cancelling a dispatch source doesn't + // invoke the cancel handler if the dispatch source is paused. + // So we have to unpause the source if needed. + // This allows the cancel handler to be run, which in turn releases the source and closes the socket. + + [self resumeSend6Source]; + [self resumeReceive6Source]; + + send6Source = NULL; + receive6Source = NULL; + + // The sockets will be closed by the cancel handlers of the corresponding source + + socket6FD = SOCKET_NULL; + + // Clear socket states + + socket6FDBytesAvailable = 0; + flags &= ~kSock6CanAcceptBytes; + + // Clear cached info + + cachedLocalAddress6 = nil; + cachedLocalHost6 = nil; + cachedLocalPort6 = 0; + } +} + +- (void)closeSockets +{ + [self closeSocket4]; + [self closeSocket6]; + + flags &= ~kDidCreateSockets; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Diagnostics +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)getLocalAddress:(NSData **)dataPtr + host:(NSString **)hostPtr + port:(uint16_t *)portPtr + forSocket:(int)socketFD + withFamily:(int)socketFamily +{ + + NSData *data = nil; + NSString *host = nil; + uint16_t port = 0; + + if (socketFamily == AF_INET) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + data = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; + host = [[self class] hostFromSockaddr4:&sockaddr4]; + port = [[self class] portFromSockaddr4:&sockaddr4]; + } + else + { + LogWarn(@"Error in getsockname: %@", [self errnoError]); + } + } + else if (socketFamily == AF_INET6) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + data = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; + host = [[self class] hostFromSockaddr6:&sockaddr6]; + port = [[self class] portFromSockaddr6:&sockaddr6]; + } + else + { + LogWarn(@"Error in getsockname: %@", [self errnoError]); + } + } + + if (dataPtr) *dataPtr = data; + if (hostPtr) *hostPtr = host; + if (portPtr) *portPtr = port; + + return (data != nil); +} + +- (void)maybeUpdateCachedLocalAddress4Info +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if ( cachedLocalAddress4 || ((flags & kDidBind) == 0) || (socket4FD == SOCKET_NULL) ) + { + return; + } + + NSData *address = nil; + NSString *host = nil; + uint16_t port = 0; + + if ([self getLocalAddress:&address host:&host port:&port forSocket:socket4FD withFamily:AF_INET]) + { + + cachedLocalAddress4 = address; + cachedLocalHost4 = host; + cachedLocalPort4 = port; + } +} + +- (void)maybeUpdateCachedLocalAddress6Info +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if ( cachedLocalAddress6 || ((flags & kDidBind) == 0) || (socket6FD == SOCKET_NULL) ) + { + return; + } + + NSData *address = nil; + NSString *host = nil; + uint16_t port = 0; + + if ([self getLocalAddress:&address host:&host port:&port forSocket:socket6FD withFamily:AF_INET6]) + { + + cachedLocalAddress6 = address; + cachedLocalHost6 = host; + cachedLocalPort6 = port; + } +} + +- (NSData *)localAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + + if (socket4FD != SOCKET_NULL) + { + [self maybeUpdateCachedLocalAddress4Info]; + result = cachedLocalAddress4; + } + else + { + [self maybeUpdateCachedLocalAddress6Info]; + result = cachedLocalAddress6; + } + + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (NSString *)localHost +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + + if (socket4FD != SOCKET_NULL) + { + [self maybeUpdateCachedLocalAddress4Info]; + result = cachedLocalHost4; + } + else + { + [self maybeUpdateCachedLocalAddress6Info]; + result = cachedLocalHost6; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (uint16_t)localPort +{ + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + if (socket4FD != SOCKET_NULL) + { + [self maybeUpdateCachedLocalAddress4Info]; + result = cachedLocalPort4; + } + else + { + [self maybeUpdateCachedLocalAddress6Info]; + result = cachedLocalPort6; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (NSData *)localAddress_IPv4 +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress4Info]; + result = cachedLocalAddress4; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (NSString *)localHost_IPv4 +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress4Info]; + result = cachedLocalHost4; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (uint16_t)localPort_IPv4 +{ + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress4Info]; + result = cachedLocalPort4; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (NSData *)localAddress_IPv6 +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress6Info]; + result = cachedLocalAddress6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (NSString *)localHost_IPv6 +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress6Info]; + result = cachedLocalHost6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (uint16_t)localPort_IPv6 +{ + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedLocalAddress6Info]; + result = cachedLocalPort6; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (void)maybeUpdateCachedConnectedAddressInfo +{ + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (cachedConnectedAddress || (flags & kDidConnect) == 0) + { + return; + } + + NSData *data = nil; + NSString *host = nil; + uint16_t port = 0; + int family = AF_UNSPEC; + + if (socket4FD != SOCKET_NULL) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) + { + data = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; + host = [[self class] hostFromSockaddr4:&sockaddr4]; + port = [[self class] portFromSockaddr4:&sockaddr4]; + family = AF_INET; + } + else + { + LogWarn(@"Error in getpeername: %@", [self errnoError]); + } + } + else if (socket6FD != SOCKET_NULL) + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) + { + data = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; + host = [[self class] hostFromSockaddr6:&sockaddr6]; + port = [[self class] portFromSockaddr6:&sockaddr6]; + family = AF_INET6; + } + else + { + LogWarn(@"Error in getpeername: %@", [self errnoError]); + } + } + + + cachedConnectedAddress = data; + cachedConnectedHost = host; + cachedConnectedPort = port; + cachedConnectedFamily = family; +} + +- (NSData *)connectedAddress +{ + __block NSData *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedConnectedAddressInfo]; + result = cachedConnectedAddress; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (NSString *)connectedHost +{ + __block NSString *result = nil; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedConnectedAddressInfo]; + result = cachedConnectedHost; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (uint16_t)connectedPort +{ + __block uint16_t result = 0; + + dispatch_block_t block = ^{ + + [self maybeUpdateCachedConnectedAddressInfo]; + result = cachedConnectedPort; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, AutoreleasedBlock(block)); + + return result; +} + +- (BOOL)isConnected +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + result = (flags & kDidConnect) ? YES : NO; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isClosed +{ + __block BOOL result = YES; + + dispatch_block_t block = ^{ + + result = (flags & kDidCreateSockets) ? NO : YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPv4 +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + + if (flags & kDidCreateSockets) + { + result = (socket4FD != SOCKET_NULL); + } + else + { + result = [self isIPv4Enabled]; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +- (BOOL)isIPv6 +{ + __block BOOL result = NO; + + dispatch_block_t block = ^{ + + if (flags & kDidCreateSockets) + { + result = (socket6FD != SOCKET_NULL); + } + else + { + result = [self isIPv6Enabled]; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Binding +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method runs through the various checks required prior to a bind attempt. + * It is shared between the various bind methods. +**/ +- (BOOL)preBind:(NSError **)errPtr +{ + if (![self preOp:errPtr]) + { + return NO; + } + + if (flags & kDidBind) + { + if (errPtr) + { + NSString *msg = @"Cannot bind a socket more than once."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if ((flags & kConnecting) || (flags & kDidConnect)) + { + if (errPtr) + { + NSString *msg = @"Cannot bind after connecting. If needed, bind first, then connect."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + if (errPtr) + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + return YES; +} + +- (BOOL)bindToPort:(uint16_t)port error:(NSError **)errPtr +{ + return [self bindToPort:port interface:nil error:errPtr]; +} + +- (BOOL)bindToPort:(uint16_t)port interface:(NSString *)interface error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks + + if (![self preBind:&err]) + { + return_from_block; + } + + // Check the given interface + + NSData *interface4 = nil; + NSData *interface6 = nil; + + [self convertIntefaceDescription:interface port:port intoAddress4:&interface4 address6:&interface6]; + + if ((interface4 == nil) && (interface6 == nil)) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && (interface6 == nil)) + { + NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && (interface4 == nil)) + { + NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Determine protocol(s) + + BOOL useIPv4 = !isIPv4Disabled && (interface4 != nil); + BOOL useIPv6 = !isIPv6Disabled && (interface6 != nil); + + // Create the socket(s) if needed + + if ((flags & kDidCreateSockets) == 0) + { + if (![self createSocket4:useIPv4 socket6:useIPv6 error:&err]) + { + return_from_block; + } + } + + // Bind the socket(s) + + LogVerbose(@"Binding socket to port(%hu) interface(%@)", port, interface); + + if (useIPv4) + { + int status = bind(socket4FD, (struct sockaddr *)[interface4 bytes], (socklen_t)[interface4 length]); + if (status == -1) + { + [self closeSockets]; + + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + return_from_block; + } + } + + if (useIPv6) + { + int status = bind(socket6FD, (struct sockaddr *)[interface6 bytes], (socklen_t)[interface6 length]); + if (status == -1) + { + [self closeSockets]; + + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + return_from_block; + } + } + + // Update flags + + flags |= kDidBind; + + if (!useIPv4) flags |= kIPv4Deactivated; + if (!useIPv6) flags |= kIPv6Deactivated; + + result = YES; + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error binding to port/interface: %@", err); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks + + if (![self preBind:&err]) + { + return_from_block; + } + + // Check the given address + + int addressFamily = [[self class] familyFromAddress:localAddr]; + + if (addressFamily == AF_UNSPEC) + { + NSString *msg = @"A valid IPv4 or IPv6 address was not given"; + err = [self badParamError:msg]; + + return_from_block; + } + + NSData *localAddr4 = (addressFamily == AF_INET) ? localAddr : nil; + NSData *localAddr6 = (addressFamily == AF_INET6) ? localAddr : nil; + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && localAddr4) + { + NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + if (isIPv6Disabled && localAddr6) + { + NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Determine protocol(s) + + BOOL useIPv4 = !isIPv4Disabled && (localAddr4 != nil); + BOOL useIPv6 = !isIPv6Disabled && (localAddr6 != nil); + + // Create the socket(s) if needed + + if ((flags & kDidCreateSockets) == 0) + { + if (![self createSocket4:useIPv4 socket6:useIPv6 error:&err]) + { + return_from_block; + } + } + + // Bind the socket(s) + + if (useIPv4) + { + LogVerbose(@"Binding socket to address(%@:%hu)", + [[self class] hostFromAddress:localAddr4], + [[self class] portFromAddress:localAddr4]); + + int status = bind(socket4FD, (struct sockaddr *)[localAddr4 bytes], (socklen_t)[localAddr4 length]); + if (status == -1) + { + [self closeSockets]; + + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + return_from_block; + } + } + else + { + LogVerbose(@"Binding socket to address(%@:%hu)", + [[self class] hostFromAddress:localAddr6], + [[self class] portFromAddress:localAddr6]); + + int status = bind(socket6FD, (struct sockaddr *)[localAddr6 bytes], (socklen_t)[localAddr6 length]); + if (status == -1) + { + [self closeSockets]; + + NSString *reason = @"Error in bind() function"; + err = [self errnoErrorWithReason:reason]; + + return_from_block; + } + } + + // Update flags + + flags |= kDidBind; + + if (!useIPv4) flags |= kIPv4Deactivated; + if (!useIPv6) flags |= kIPv6Deactivated; + + result = YES; + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error binding to address: %@", err); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * This method runs through the various checks required prior to a connect attempt. + * It is shared between the various connect methods. +**/ +- (BOOL)preConnect:(NSError **)errPtr +{ + if (![self preOp:errPtr]) + { + return NO; + } + + if ((flags & kConnecting) || (flags & kDidConnect)) + { + if (errPtr) + { + NSString *msg = @"Cannot connect a socket more than once."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; + BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; + + if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled + { + if (errPtr) + { + NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + return YES; +} + +- (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks. + + if (![self preConnect:&err]) + { + return_from_block; + } + + // Check parameter(s) + + if (host == nil) + { + NSString *msg = @"The host param is nil. Should be domain name or IP address string."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Create the socket(s) if needed + + if ((flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } + + // Create special connect packet + + GCDAsyncUdpSpecialPacket *packet = [[GCDAsyncUdpSpecialPacket alloc] init]; + packet->resolveInProgress = YES; + + // Start asynchronous DNS resolve for host:port on background queue + + LogVerbose(@"Dispatching DNS resolve for connect..."); + + [self asyncResolveHost:host port:port withCompletionBlock:^(NSArray *addresses, NSError *error) { + + // The asyncResolveHost:port:: method asynchronously dispatches a task onto the global concurrent queue, + // and immediately returns. Once the async resolve task completes, + // this block is executed on our socketQueue. + + packet->resolveInProgress = NO; + + packet->addresses = addresses; + packet->error = error; + + [self maybeConnect]; + }]; + + // Updates flags, add connect packet to send queue, and pump send queue + + flags |= kConnecting; + + [sendQueue addObject:packet]; + [self maybeDequeueSend]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error connecting to host/port: %@", err); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks. + + if (![self preConnect:&err]) + { + return_from_block; + } + + // Check parameter(s) + + if (remoteAddr == nil) + { + NSString *msg = @"The address param is nil. Should be a valid address."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Create the socket(s) if needed + + if ((flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } + + // The remoteAddr parameter could be of type NSMutableData. + // So we copy it to be safe. + + NSData *address = [remoteAddr copy]; + NSArray *addresses = [NSArray arrayWithObject:address]; + + GCDAsyncUdpSpecialPacket *packet = [[GCDAsyncUdpSpecialPacket alloc] init]; + packet->addresses = addresses; + + // Updates flags, add connect packet to send queue, and pump send queue + + flags |= kConnecting; + + [sendQueue addObject:packet]; + [self maybeDequeueSend]; + + result = YES; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error connecting to address: %@", err); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (void)maybeConnect +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + + BOOL sendQueueReady = [currentSend isKindOfClass:[GCDAsyncUdpSpecialPacket class]]; + + if (sendQueueReady) + { + GCDAsyncUdpSpecialPacket *connectPacket = (GCDAsyncUdpSpecialPacket *)currentSend; + + if (connectPacket->resolveInProgress) + { + LogVerbose(@"Waiting for DNS resolve..."); + } + else + { + if (connectPacket->error) + { + [self notifyDidNotConnect:connectPacket->error]; + } + else + { + NSData *address = nil; + NSError *error = nil; + + int addressFamily = [self getAddress:&address error:&error fromAddresses:connectPacket->addresses]; + + // Perform connect + + BOOL result = NO; + + switch (addressFamily) + { + case AF_INET : result = [self connectWithAddress4:address error:&error]; break; + case AF_INET6 : result = [self connectWithAddress6:address error:&error]; break; + } + + if (result) + { + flags |= kDidBind; + flags |= kDidConnect; + + cachedConnectedAddress = address; + cachedConnectedHost = [[self class] hostFromAddress:address]; + cachedConnectedPort = [[self class] portFromAddress:address]; + cachedConnectedFamily = addressFamily; + + [self notifyDidConnectToAddress:address]; + } + else + { + [self notifyDidNotConnect:error]; + } + } + + flags &= ~kConnecting; + + [self endCurrentSend]; + [self maybeDequeueSend]; + } + } +} + +- (BOOL)connectWithAddress4:(NSData *)address4 error:(NSError **)errPtr +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + int status = connect(socket4FD, (struct sockaddr *)[address4 bytes], (socklen_t)[address4 length]); + if (status != 0) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in connect() function"]; + + return NO; + } + + [self closeSocket6]; + flags |= kIPv6Deactivated; + + return YES; +} + +- (BOOL)connectWithAddress6:(NSData *)address6 error:(NSError **)errPtr +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + int status = connect(socket6FD, (struct sockaddr *)[address6 bytes], (socklen_t)[address6 length]); + if (status != 0) + { + if (errPtr) + *errPtr = [self errnoErrorWithReason:@"Error in connect() function"]; + + return NO; + } + + [self closeSocket4]; + flags |= kIPv4Deactivated; + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Multicast +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)preJoin:(NSError **)errPtr +{ + if (![self preOp:errPtr]) + { + return NO; + } + + if (!(flags & kDidBind)) + { + if (errPtr) + { + NSString *msg = @"Must bind a socket before joining a multicast group."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + if ((flags & kConnecting) || (flags & kDidConnect)) + { + if (errPtr) + { + NSString *msg = @"Cannot join a multicast group if connected."; + *errPtr = [self badConfigError:msg]; + } + return NO; + } + + return YES; +} + +- (BOOL)joinMulticastGroup:(NSString *)group error:(NSError **)errPtr +{ + return [self joinMulticastGroup:group onInterface:nil error:errPtr]; +} + +- (BOOL)joinMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr +{ + // IP_ADD_MEMBERSHIP == IPV6_JOIN_GROUP + return [self performMulticastRequest:IP_ADD_MEMBERSHIP forGroup:group onInterface:interface error:errPtr]; +} + +- (BOOL)leaveMulticastGroup:(NSString *)group error:(NSError **)errPtr +{ + return [self leaveMulticastGroup:group onInterface:nil error:errPtr]; +} + +- (BOOL)leaveMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr +{ + // IP_DROP_MEMBERSHIP == IPV6_LEAVE_GROUP + return [self performMulticastRequest:IP_DROP_MEMBERSHIP forGroup:group onInterface:interface error:errPtr]; +} + +- (BOOL)performMulticastRequest:(int)requestType + forGroup:(NSString *)group + onInterface:(NSString *)interface + error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + // Run through sanity checks + + if (![self preJoin:&err]) + { + return_from_block; + } + + // Convert group to address + + NSData *groupAddr4 = nil; + NSData *groupAddr6 = nil; + + [self convertNumericHost:group port:0 intoAddress4:&groupAddr4 address6:&groupAddr6]; + + if ((groupAddr4 == nil) && (groupAddr6 == nil)) + { + NSString *msg = @"Unknown group. Specify valid group IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Convert interface to address + + NSData *interfaceAddr4 = nil; + NSData *interfaceAddr6 = nil; + + [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6]; + + if ((interfaceAddr4 == nil) && (interfaceAddr6 == nil)) + { + NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; + err = [self badParamError:msg]; + + return_from_block; + } + + // Perform join + + if ((socket4FD != SOCKET_NULL) && groupAddr4 && interfaceAddr4) + { + const struct sockaddr_in *nativeGroup = (struct sockaddr_in *)[groupAddr4 bytes]; + const struct sockaddr_in *nativeIface = (struct sockaddr_in *)[interfaceAddr4 bytes]; + + struct ip_mreq imreq; + imreq.imr_multiaddr = nativeGroup->sin_addr; + imreq.imr_interface = nativeIface->sin_addr; + + int status = setsockopt(socket4FD, IPPROTO_IP, requestType, (const void *)&imreq, sizeof(imreq)); + if (status != 0) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + + // Using IPv4 only + [self closeSocket6]; + + result = YES; + } + else if ((socket6FD != SOCKET_NULL) && groupAddr6 && interfaceAddr6) + { + const struct sockaddr_in6 *nativeGroup = (struct sockaddr_in6 *)[groupAddr6 bytes]; + + struct ipv6_mreq imreq; + imreq.ipv6mr_multiaddr = nativeGroup->sin6_addr; + imreq.ipv6mr_interface = [self indexOfInterfaceAddr6:interfaceAddr6]; + + int status = setsockopt(socket6FD, IPPROTO_IPV6, requestType, (const void *)&imreq, sizeof(imreq)); + if (status != 0) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + + // Using IPv6 only + [self closeSocket4]; + + result = YES; + } + else + { + NSString *msg = @"Socket, group, and interface do not have matching IP versions"; + err = [self badParamError:msg]; + + return_from_block; + } + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Reuse port +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)enableReusePort:(BOOL)flag error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (![self preOp:&err]) + { + return_from_block; + } + + if ((flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } + + int value = flag ? 1 : 0; + if (socket4FD != SOCKET_NULL) + { + int error = setsockopt(socket4FD, SOL_SOCKET, SO_REUSEPORT, (const void *)&value, sizeof(value)); + + if (error) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + result = YES; + } + + if (socket6FD != SOCKET_NULL) + { + int error = setsockopt(socket6FD, SOL_SOCKET, SO_REUSEPORT, (const void *)&value, sizeof(value)); + + if (error) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + result = YES; + } + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Broadcast +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr +{ + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ @autoreleasepool { + + if (![self preOp:&err]) + { + return_from_block; + } + + if ((flags & kDidCreateSockets) == 0) + { + if (![self createSockets:&err]) + { + return_from_block; + } + } + + if (socket4FD != SOCKET_NULL) + { + int value = flag ? 1 : 0; + int error = setsockopt(socket4FD, SOL_SOCKET, SO_BROADCAST, (const void *)&value, sizeof(value)); + + if (error) + { + err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; + + return_from_block; + } + result = YES; + } + + // IPv6 does not implement broadcast, the ability to send a packet to all hosts on the attached link. + // The same effect can be achieved by sending a packet to the link-local all hosts multicast group. + + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (errPtr) + *errPtr = err; + + return result; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Sending +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)sendData:(NSData *)data withTag:(long)tag +{ + [self sendData:data withTimeout:-1.0 tag:tag]; +} + +- (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + LogTrace(); + + if ([data length] == 0) + { + LogWarn(@"Ignoring attempt to send nil/empty data."); + return; + } + + GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [sendQueue addObject:packet]; + [self maybeDequeueSend]; + }}); + +} + +- (void)sendData:(NSData *)data + toHost:(NSString *)host + port:(uint16_t)port + withTimeout:(NSTimeInterval)timeout + tag:(long)tag +{ + LogTrace(); + + if ([data length] == 0) + { + LogWarn(@"Ignoring attempt to send nil/empty data."); + return; + } + + GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; + packet->resolveInProgress = YES; + + [self asyncResolveHost:host port:port withCompletionBlock:^(NSArray *addresses, NSError *error) { + + // The asyncResolveHost:port:: method asynchronously dispatches a task onto the global concurrent queue, + // and immediately returns. Once the async resolve task completes, + // this block is executed on our socketQueue. + + packet->resolveInProgress = NO; + + packet->resolvedAddresses = addresses; + packet->resolveError = error; + + if (packet == currentSend) + { + LogVerbose(@"currentSend - address resolved"); + [self doPreSend]; + } + }]; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [sendQueue addObject:packet]; + [self maybeDequeueSend]; + + }}); + +} + +- (void)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + LogTrace(); + + if ([data length] == 0) + { + LogWarn(@"Ignoring attempt to send nil/empty data."); + return; + } + + GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; + packet->addressFamily = [GCDAsyncUdpSocket familyFromAddress:remoteAddr]; + packet->address = remoteAddr; + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [sendQueue addObject:packet]; + [self maybeDequeueSend]; + }}); +} + +- (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue +{ + [self setSendFilter:filterBlock withQueue:filterQueue isAsynchronous:YES]; +} + +- (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock + withQueue:(dispatch_queue_t)filterQueue + isAsynchronous:(BOOL)isAsynchronous +{ + GCDAsyncUdpSocketSendFilterBlock newFilterBlock = NULL; + dispatch_queue_t newFilterQueue = NULL; + + if (filterBlock) + { + NSAssert(filterQueue, @"Must provide a dispatch_queue in which to run the filter block."); + + newFilterBlock = [filterBlock copy]; + newFilterQueue = filterQueue; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(newFilterQueue); + #endif + } + + dispatch_block_t block = ^{ + + #if !OS_OBJECT_USE_OBJC + if (sendFilterQueue) dispatch_release(sendFilterQueue); + #endif + + sendFilterBlock = newFilterBlock; + sendFilterQueue = newFilterQueue; + sendFilterAsync = isAsynchronous; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (void)maybeDequeueSend +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + // If we don't have a send operation already in progress + if (currentSend == nil) + { + // Create the sockets if needed + if ((flags & kDidCreateSockets) == 0) + { + NSError *err = nil; + if (![self createSockets:&err]) + { + [self closeWithError:err]; + return; + } + } + + while ([sendQueue count] > 0) + { + // Dequeue the next object in the queue + currentSend = [sendQueue objectAtIndex:0]; + [sendQueue removeObjectAtIndex:0]; + + if ([currentSend isKindOfClass:[GCDAsyncUdpSpecialPacket class]]) + { + [self maybeConnect]; + + return; // The maybeConnect method, if it connects, will invoke this method again + } + else if (currentSend->resolveError) + { + // Notify delegate + [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:currentSend->resolveError]; + + // Clear currentSend + currentSend = nil; + + continue; + } + else + { + // Start preprocessing checks on the send packet + [self doPreSend]; + + break; + } + } + + if ((currentSend == nil) && (flags & kCloseAfterSends)) + { + [self closeWithError:nil]; + } + } +} + +/** + * This method is called after a sendPacket has been dequeued. + * It performs various preprocessing checks on the packet, + * and queries the sendFilter (if set) to determine if the packet can be sent. + * + * If the packet passes all checks, it will be passed on to the doSend method. +**/ +- (void)doPreSend +{ + LogTrace(); + + // + // 1. Check for problems with send packet + // + + BOOL waitingForResolve = NO; + NSError *error = nil; + + if (flags & kDidConnect) + { + // Connected socket + + if (currentSend->resolveInProgress || currentSend->resolvedAddresses || currentSend->resolveError) + { + NSString *msg = @"Cannot specify destination of packet for connected socket"; + error = [self badConfigError:msg]; + } + else + { + currentSend->address = cachedConnectedAddress; + currentSend->addressFamily = cachedConnectedFamily; + } + } + else + { + // Non-Connected socket + + if (currentSend->resolveInProgress) + { + // We're waiting for the packet's destination to be resolved. + waitingForResolve = YES; + } + else if (currentSend->resolveError) + { + error = currentSend->resolveError; + } + else if (currentSend->address == nil) + { + if (currentSend->resolvedAddresses == nil) + { + NSString *msg = @"You must specify destination of packet for a non-connected socket"; + error = [self badConfigError:msg]; + } + else + { + // Pick the proper address to use (out of possibly several resolved addresses) + + NSData *address = nil; + int addressFamily = AF_UNSPEC; + + addressFamily = [self getAddress:&address error:&error fromAddresses:currentSend->resolvedAddresses]; + + currentSend->address = address; + currentSend->addressFamily = addressFamily; + } + } + } + + if (waitingForResolve) + { + // We're waiting for the packet's destination to be resolved. + + LogVerbose(@"currentSend - waiting for address resolve"); + + if (flags & kSock4CanAcceptBytes) { + [self suspendSend4Source]; + } + if (flags & kSock6CanAcceptBytes) { + [self suspendSend6Source]; + } + + return; + } + + if (error) + { + // Unable to send packet due to some error. + // Notify delegate and move on. + + [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:error]; + [self endCurrentSend]; + [self maybeDequeueSend]; + + return; + } + + // + // 2. Query sendFilter (if applicable) + // + + if (sendFilterBlock && sendFilterQueue) + { + // Query sendFilter + + if (sendFilterAsync) + { + // Scenario 1 of 3 - Need to asynchronously query sendFilter + + currentSend->filterInProgress = YES; + GCDAsyncUdpSendPacket *sendPacket = currentSend; + + dispatch_async(sendFilterQueue, ^{ @autoreleasepool { + + BOOL allowed = sendFilterBlock(sendPacket->buffer, sendPacket->address, sendPacket->tag); + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + sendPacket->filterInProgress = NO; + if (sendPacket == currentSend) + { + if (allowed) + { + [self doSend]; + } + else + { + LogVerbose(@"currentSend - silently dropped by sendFilter"); + + [self notifyDidSendDataWithTag:currentSend->tag]; + [self endCurrentSend]; + [self maybeDequeueSend]; + } + } + }}); + }}); + } + else + { + // Scenario 2 of 3 - Need to synchronously query sendFilter + + __block BOOL allowed = YES; + + dispatch_sync(sendFilterQueue, ^{ @autoreleasepool { + + allowed = sendFilterBlock(currentSend->buffer, currentSend->address, currentSend->tag); + }}); + + if (allowed) + { + [self doSend]; + } + else + { + LogVerbose(@"currentSend - silently dropped by sendFilter"); + + [self notifyDidSendDataWithTag:currentSend->tag]; + [self endCurrentSend]; + [self maybeDequeueSend]; + } + } + } + else // if (!sendFilterBlock || !sendFilterQueue) + { + // Scenario 3 of 3 - No sendFilter. Just go straight into sending. + + [self doSend]; + } +} + +/** + * This method performs the actual sending of data in the currentSend packet. + * It should only be called if the +**/ +- (void)doSend +{ + LogTrace(); + + NSAssert(currentSend != nil, @"Invalid logic"); + + // Perform the actual send + + ssize_t result = 0; + + if (flags & kDidConnect) + { + // Connected socket + + const void *buffer = [currentSend->buffer bytes]; + size_t length = (size_t)[currentSend->buffer length]; + + if (currentSend->addressFamily == AF_INET) + { + result = send(socket4FD, buffer, length, 0); + LogVerbose(@"send(socket4FD) = %d", result); + } + else + { + result = send(socket6FD, buffer, length, 0); + LogVerbose(@"send(socket6FD) = %d", result); + } + } + else + { + // Non-Connected socket + + const void *buffer = [currentSend->buffer bytes]; + size_t length = (size_t)[currentSend->buffer length]; + + const void *dst = [currentSend->address bytes]; + socklen_t dstSize = (socklen_t)[currentSend->address length]; + + if (currentSend->addressFamily == AF_INET) + { + result = sendto(socket4FD, buffer, length, 0, dst, dstSize); + LogVerbose(@"sendto(socket4FD) = %d", result); + } + else + { + result = sendto(socket6FD, buffer, length, 0, dst, dstSize); + LogVerbose(@"sendto(socket6FD) = %d", result); + } + } + + // If the socket wasn't bound before, it is now + + if ((flags & kDidBind) == 0) + { + flags |= kDidBind; + } + + // Check the results. + // + // From the send() & sendto() manpage: + // + // Upon successful completion, the number of bytes which were sent is returned. + // Otherwise, -1 is returned and the global variable errno is set to indicate the error. + + BOOL waitingForSocket = NO; + NSError *socketError = nil; + + if (result == 0) + { + waitingForSocket = YES; + } + else if (result < 0) + { + if (errno == EAGAIN) + waitingForSocket = YES; + else + socketError = [self errnoErrorWithReason:@"Error in send() function."]; + } + + if (waitingForSocket) + { + // Not enough room in the underlying OS socket send buffer. + // Wait for a notification of available space. + + LogVerbose(@"currentSend - waiting for socket"); + + if (!(flags & kSock4CanAcceptBytes)) { + [self resumeSend4Source]; + } + if (!(flags & kSock6CanAcceptBytes)) { + [self resumeSend6Source]; + } + + if ((sendTimer == NULL) && (currentSend->timeout >= 0.0)) + { + // Unable to send packet right away. + // Start timer to timeout the send operation. + + [self setupSendTimerWithTimeout:currentSend->timeout]; + } + } + else if (socketError) + { + [self closeWithError:socketError]; + } + else // done + { + [self notifyDidSendDataWithTag:currentSend->tag]; + [self endCurrentSend]; + [self maybeDequeueSend]; + } +} + +/** + * Releases all resources associated with the currentSend. +**/ +- (void)endCurrentSend +{ + if (sendTimer) + { + dispatch_source_cancel(sendTimer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(sendTimer); + #endif + sendTimer = NULL; + } + + currentSend = nil; +} + +/** + * Performs the operations to timeout the current send operation, and move on. +**/ +- (void)doSendTimeout +{ + LogTrace(); + + [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:[self sendTimeoutError]]; + [self endCurrentSend]; + [self maybeDequeueSend]; +} + +/** + * Sets up a timer that fires to timeout the current send operation. + * This method should only be called once per send packet. +**/ +- (void)setupSendTimerWithTimeout:(NSTimeInterval)timeout +{ + NSAssert(sendTimer == NULL, @"Invalid logic"); + NSAssert(timeout >= 0.0, @"Invalid logic"); + + LogTrace(); + + sendTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); + + dispatch_source_set_event_handler(sendTimer, ^{ @autoreleasepool { + + [self doSendTimeout]; + }}); + + dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); + + dispatch_source_set_timer(sendTimer, tt, DISPATCH_TIME_FOREVER, 0); + dispatch_resume(sendTimer); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Receiving +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)receiveOnce:(NSError **)errPtr +{ + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ + + if ((flags & kReceiveOnce) == 0) + { + if ((flags & kDidCreateSockets) == 0) + { + NSString *msg = @"Must bind socket before you can receive data. " + @"You can do this explicitly via bind, or implicitly via connect or by sending data."; + + err = [self badConfigError:msg]; + return_from_block; + } + + flags |= kReceiveOnce; // Enable + flags &= ~kReceiveContinuous; // Disable + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self doReceive]; + }}); + } + + result = YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error in beginReceiving: %@", err); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (BOOL)beginReceiving:(NSError **)errPtr +{ + LogTrace(); + + __block BOOL result = NO; + __block NSError *err = nil; + + dispatch_block_t block = ^{ + + if ((flags & kReceiveContinuous) == 0) + { + if ((flags & kDidCreateSockets) == 0) + { + NSString *msg = @"Must bind socket before you can receive data. " + @"You can do this explicitly via bind, or implicitly via connect or by sending data."; + + err = [self badConfigError:msg]; + return_from_block; + } + + flags |= kReceiveContinuous; // Enable + flags &= ~kReceiveOnce; // Disable + + dispatch_async(socketQueue, ^{ @autoreleasepool { + + [self doReceive]; + }}); + } + + result = YES; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); + + if (err) + LogError(@"Error in beginReceiving: %@", err); + + if (errPtr) + *errPtr = err; + + return result; +} + +- (void)pauseReceiving +{ + LogTrace(); + + dispatch_block_t block = ^{ + + flags &= ~kReceiveOnce; // Disable + flags &= ~kReceiveContinuous; // Disable + + if (socket4FDBytesAvailable > 0) { + [self suspendReceive4Source]; + } + if (socket6FDBytesAvailable > 0) { + [self suspendReceive6Source]; + } + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue +{ + [self setReceiveFilter:filterBlock withQueue:filterQueue isAsynchronous:YES]; +} + +- (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock + withQueue:(dispatch_queue_t)filterQueue + isAsynchronous:(BOOL)isAsynchronous +{ + GCDAsyncUdpSocketReceiveFilterBlock newFilterBlock = NULL; + dispatch_queue_t newFilterQueue = NULL; + + if (filterBlock) + { + NSAssert(filterQueue, @"Must provide a dispatch_queue in which to run the filter block."); + + newFilterBlock = [filterBlock copy]; + newFilterQueue = filterQueue; + #if !OS_OBJECT_USE_OBJC + dispatch_retain(newFilterQueue); + #endif + } + + dispatch_block_t block = ^{ + + #if !OS_OBJECT_USE_OBJC + if (receiveFilterQueue) dispatch_release(receiveFilterQueue); + #endif + + receiveFilterBlock = newFilterBlock; + receiveFilterQueue = newFilterQueue; + receiveFilterAsync = isAsynchronous; + }; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +- (void)doReceive +{ + LogTrace(); + + if ((flags & (kReceiveOnce | kReceiveContinuous)) == 0) + { + LogVerbose(@"Receiving is paused..."); + + if (socket4FDBytesAvailable > 0) { + [self suspendReceive4Source]; + } + if (socket6FDBytesAvailable > 0) { + [self suspendReceive6Source]; + } + + return; + } + + if ((flags & kReceiveOnce) && (pendingFilterOperations > 0)) + { + LogVerbose(@"Receiving is temporarily paused (pending filter operations)..."); + + if (socket4FDBytesAvailable > 0) { + [self suspendReceive4Source]; + } + if (socket6FDBytesAvailable > 0) { + [self suspendReceive6Source]; + } + + return; + } + + if ((socket4FDBytesAvailable == 0) && (socket6FDBytesAvailable == 0)) + { + LogVerbose(@"No data available to receive..."); + + if (socket4FDBytesAvailable == 0) { + [self resumeReceive4Source]; + } + if (socket6FDBytesAvailable == 0) { + [self resumeReceive6Source]; + } + + return; + } + + // Figure out if we should receive on socket4 or socket6 + + BOOL doReceive4; + + if (flags & kDidConnect) + { + // Connected socket + + doReceive4 = (socket4FD != SOCKET_NULL); + } + else + { + // Non-Connected socket + + if (socket4FDBytesAvailable > 0) + { + if (socket6FDBytesAvailable > 0) + { + // Bytes available on socket4 & socket6 + + doReceive4 = (flags & kFlipFlop) ? YES : NO; + + flags ^= kFlipFlop; // flags = flags xor kFlipFlop; (toggle flip flop bit) + } + else { + // Bytes available on socket4, but not socket6 + doReceive4 = YES; + } + } + else { + // Bytes available on socket6, but not socket4 + doReceive4 = NO; + } + } + + // Perform socket IO + + ssize_t result = 0; + + NSData *data = nil; + NSData *addr4 = nil; + NSData *addr6 = nil; + + if (doReceive4) + { + NSAssert(socket4FDBytesAvailable > 0, @"Invalid logic"); + LogVerbose(@"Receiving on IPv4"); + + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + // #222: GCD does not necessarily return the size of an entire UDP packet + // from dispatch_source_get_data(), so we must use the maximum packet size. + size_t bufSize = max4ReceiveSize; + void *buf = malloc(bufSize); + + result = recvfrom(socket4FD, buf, bufSize, 0, (struct sockaddr *)&sockaddr4, &sockaddr4len); + LogVerbose(@"recvfrom(socket4FD) = %i", (int)result); + + if (result > 0) + { + if ((size_t)result >= socket4FDBytesAvailable) + socket4FDBytesAvailable = 0; + else + socket4FDBytesAvailable -= result; + + if ((size_t)result != bufSize) { + buf = realloc(buf, result); + } + + data = [NSData dataWithBytesNoCopy:buf length:result freeWhenDone:YES]; + addr4 = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; + } + else + { + LogVerbose(@"recvfrom(socket4FD) = %@", [self errnoError]); + socket4FDBytesAvailable = 0; + free(buf); + } + } + else + { + NSAssert(socket6FDBytesAvailable > 0, @"Invalid logic"); + LogVerbose(@"Receiving on IPv6"); + + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + // #222: GCD does not necessarily return the size of an entire UDP packet + // from dispatch_source_get_data(), so we must use the maximum packet size. + size_t bufSize = max6ReceiveSize; + void *buf = malloc(bufSize); + + result = recvfrom(socket6FD, buf, bufSize, 0, (struct sockaddr *)&sockaddr6, &sockaddr6len); + LogVerbose(@"recvfrom(socket6FD) -> %i", (int)result); + + if (result > 0) + { + if ((size_t)result >= socket6FDBytesAvailable) + socket6FDBytesAvailable = 0; + else + socket6FDBytesAvailable -= result; + + if ((size_t)result != bufSize) { + buf = realloc(buf, result); + } + + data = [NSData dataWithBytesNoCopy:buf length:result freeWhenDone:YES]; + addr6 = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; + } + else + { + LogVerbose(@"recvfrom(socket6FD) = %@", [self errnoError]); + socket6FDBytesAvailable = 0; + free(buf); + } + } + + + BOOL waitingForSocket = NO; + BOOL notifiedDelegate = NO; + BOOL ignored = NO; + + NSError *socketError = nil; + + if (result == 0) + { + waitingForSocket = YES; + } + else if (result < 0) + { + if (errno == EAGAIN) + waitingForSocket = YES; + else + socketError = [self errnoErrorWithReason:@"Error in recvfrom() function"]; + } + else + { + if (flags & kDidConnect) + { + if (addr4 && ![self isConnectedToAddress4:addr4]) + ignored = YES; + if (addr6 && ![self isConnectedToAddress6:addr6]) + ignored = YES; + } + + NSData *addr = (addr4 != nil) ? addr4 : addr6; + + if (!ignored) + { + if (receiveFilterBlock && receiveFilterQueue) + { + // Run data through filter, and if approved, notify delegate + + __block id filterContext = nil; + __block BOOL allowed = NO; + + if (receiveFilterAsync) + { + pendingFilterOperations++; + dispatch_async(receiveFilterQueue, ^{ @autoreleasepool { + + allowed = receiveFilterBlock(data, addr, &filterContext); + + // Transition back to socketQueue to get the current delegate / delegateQueue + dispatch_async(socketQueue, ^{ @autoreleasepool { + + pendingFilterOperations--; + + if (allowed) + { + [self notifyDidReceiveData:data fromAddress:addr withFilterContext:filterContext]; + } + else + { + LogVerbose(@"received packet silently dropped by receiveFilter"); + } + + if (flags & kReceiveOnce) + { + if (allowed) + { + // The delegate has been notified, + // so our receive once operation has completed. + flags &= ~kReceiveOnce; + } + else if (pendingFilterOperations == 0) + { + // All pending filter operations have completed, + // and none were allowed through. + // Our receive once operation hasn't completed yet. + [self doReceive]; + } + } + }}); + }}); + } + else // if (!receiveFilterAsync) + { + dispatch_sync(receiveFilterQueue, ^{ @autoreleasepool { + + allowed = receiveFilterBlock(data, addr, &filterContext); + }}); + + if (allowed) + { + [self notifyDidReceiveData:data fromAddress:addr withFilterContext:filterContext]; + notifiedDelegate = YES; + } + else + { + LogVerbose(@"received packet silently dropped by receiveFilter"); + ignored = YES; + } + } + } + else // if (!receiveFilterBlock || !receiveFilterQueue) + { + [self notifyDidReceiveData:data fromAddress:addr withFilterContext:nil]; + notifiedDelegate = YES; + } + } + } + + if (waitingForSocket) + { + // Wait for a notification of available data. + + if (socket4FDBytesAvailable == 0) { + [self resumeReceive4Source]; + } + if (socket6FDBytesAvailable == 0) { + [self resumeReceive6Source]; + } + } + else if (socketError) + { + [self closeWithError:socketError]; + } + else + { + if (flags & kReceiveContinuous) + { + // Continuous receive mode + [self doReceive]; + } + else + { + // One-at-a-time receive mode + if (notifiedDelegate) + { + // The delegate has been notified (no set filter). + // So our receive once operation has completed. + flags &= ~kReceiveOnce; + } + else if (ignored) + { + [self doReceive]; + } + else + { + // Waiting on asynchronous receive filter... + } + } + } +} + +- (void)doReceiveEOF +{ + LogTrace(); + + [self closeWithError:[self socketClosedError]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Closing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)closeWithError:(NSError *)error +{ + LogVerbose(@"closeWithError: %@", error); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (currentSend) [self endCurrentSend]; + + [sendQueue removeAllObjects]; + + // If a socket has been created, we should notify the delegate. + BOOL shouldCallDelegate = (flags & kDidCreateSockets) ? YES : NO; + + // Close all sockets, send/receive sources, cfstreams, etc +#if TARGET_OS_IPHONE + [self removeStreamsFromRunLoop]; + [self closeReadAndWriteStreams]; +#endif + [self closeSockets]; + + // Clear all flags (config remains as is) + flags = 0; + + if (shouldCallDelegate) + { + [self notifyDidCloseWithError:error]; + } +} + +- (void)close +{ + LogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + [self closeWithError:nil]; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +- (void)closeAfterSending +{ + LogTrace(); + + dispatch_block_t block = ^{ @autoreleasepool { + + flags |= kCloseAfterSends; + + if (currentSend == nil && [sendQueue count] == 0) + { + [self closeWithError:nil]; + } + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark CFStream +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if TARGET_OS_IPHONE + +static NSThread *listenerThread; + ++ (void)ignore:(id)_ +{} + ++ (void)startListenerThreadIfNeeded +{ + static dispatch_once_t predicate; + dispatch_once(&predicate, ^{ + + listenerThread = [[NSThread alloc] initWithTarget:self + selector:@selector(listenerThread) + object:nil]; + [listenerThread start]; + }); +} + ++ (void)listenerThread +{ + @autoreleasepool { + + [[NSThread currentThread] setName:GCDAsyncUdpSocketThreadName]; + + LogInfo(@"ListenerThread: Started"); + + // We can't run the run loop unless it has an associated input source or a timer. + // So we'll just create a timer that will never fire - unless the server runs for a decades. + [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] + target:self + selector:@selector(ignore:) + userInfo:nil + repeats:YES]; + + [[NSRunLoop currentRunLoop] run]; + + LogInfo(@"ListenerThread: Stopped"); + } +} + ++ (void)addStreamListener:(GCDAsyncUdpSocket *)asyncUdpSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == listenerThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncUdpSocket->readStream4) + CFReadStreamScheduleWithRunLoop(asyncUdpSocket->readStream4, runLoop, kCFRunLoopDefaultMode); + + if (asyncUdpSocket->readStream6) + CFReadStreamScheduleWithRunLoop(asyncUdpSocket->readStream6, runLoop, kCFRunLoopDefaultMode); + + if (asyncUdpSocket->writeStream4) + CFWriteStreamScheduleWithRunLoop(asyncUdpSocket->writeStream4, runLoop, kCFRunLoopDefaultMode); + + if (asyncUdpSocket->writeStream6) + CFWriteStreamScheduleWithRunLoop(asyncUdpSocket->writeStream6, runLoop, kCFRunLoopDefaultMode); +} + ++ (void)removeStreamListener:(GCDAsyncUdpSocket *)asyncUdpSocket +{ + LogTrace(); + NSAssert([NSThread currentThread] == listenerThread, @"Invoked on wrong thread"); + + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + + if (asyncUdpSocket->readStream4) + CFReadStreamUnscheduleFromRunLoop(asyncUdpSocket->readStream4, runLoop, kCFRunLoopDefaultMode); + + if (asyncUdpSocket->readStream6) + CFReadStreamUnscheduleFromRunLoop(asyncUdpSocket->readStream6, runLoop, kCFRunLoopDefaultMode); + + if (asyncUdpSocket->writeStream4) + CFWriteStreamUnscheduleFromRunLoop(asyncUdpSocket->writeStream4, runLoop, kCFRunLoopDefaultMode); + + if (asyncUdpSocket->writeStream6) + CFWriteStreamUnscheduleFromRunLoop(asyncUdpSocket->writeStream6, runLoop, kCFRunLoopDefaultMode); +} + +static void CFReadStreamCallback(CFReadStreamRef stream, CFStreamEventType type, void *pInfo) +{ + @autoreleasepool { + GCDAsyncUdpSocket *asyncUdpSocket = (__bridge GCDAsyncUdpSocket *)pInfo; + + switch(type) + { + case kCFStreamEventOpenCompleted: + { + LogCVerbose(@"CFReadStreamCallback - Open"); + break; + } + case kCFStreamEventHasBytesAvailable: + { + LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); + break; + } + case kCFStreamEventErrorOccurred: + case kCFStreamEventEndEncountered: + { + NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncUdpSocket socketClosedError]; + } + + dispatch_async(asyncUdpSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFReadStreamCallback - %@", + (type == kCFStreamEventErrorOccurred) ? @"Error" : @"EndEncountered"); + + if (stream != asyncUdpSocket->readStream4 && + stream != asyncUdpSocket->readStream6 ) + { + LogCVerbose(@"CFReadStreamCallback - Ignored"); + return_from_block; + } + + [asyncUdpSocket closeWithError:error]; + + }}); + + break; + } + default: + { + LogCError(@"CFReadStreamCallback - UnknownType: %i", (int)type); + } + } + } +} + +static void CFWriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) +{ + @autoreleasepool { + GCDAsyncUdpSocket *asyncUdpSocket = (__bridge GCDAsyncUdpSocket *)pInfo; + + switch(type) + { + case kCFStreamEventOpenCompleted: + { + LogCVerbose(@"CFWriteStreamCallback - Open"); + break; + } + case kCFStreamEventCanAcceptBytes: + { + LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); + break; + } + case kCFStreamEventErrorOccurred: + case kCFStreamEventEndEncountered: + { + NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); + if (error == nil && type == kCFStreamEventEndEncountered) + { + error = [asyncUdpSocket socketClosedError]; + } + + dispatch_async(asyncUdpSocket->socketQueue, ^{ @autoreleasepool { + + LogCVerbose(@"CFWriteStreamCallback - %@", + (type == kCFStreamEventErrorOccurred) ? @"Error" : @"EndEncountered"); + + if (stream != asyncUdpSocket->writeStream4 && + stream != asyncUdpSocket->writeStream6 ) + { + LogCVerbose(@"CFWriteStreamCallback - Ignored"); + return_from_block; + } + + [asyncUdpSocket closeWithError:error]; + + }}); + + break; + } + default: + { + LogCError(@"CFWriteStreamCallback - UnknownType: %i", (int)type); + } + } + } +} + +- (BOOL)createReadAndWriteStreams:(NSError **)errPtr +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + NSError *err = nil; + + if (readStream4 || writeStream4 || readStream6 || writeStream6) + { + // Streams already created + return YES; + } + + if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) + { + err = [self otherError:@"Cannot create streams without a file descriptor"]; + goto Failed; + } + + // Create streams + + LogVerbose(@"Creating read and write stream(s)..."); + + if (socket4FD != SOCKET_NULL) + { + CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socket4FD, &readStream4, &writeStream4); + if (!readStream4 || !writeStream4) + { + err = [self otherError:@"Error in CFStreamCreatePairWithSocket() [IPv4]"]; + goto Failed; + } + } + + if (socket6FD != SOCKET_NULL) + { + CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socket6FD, &readStream6, &writeStream6); + if (!readStream6 || !writeStream6) + { + err = [self otherError:@"Error in CFStreamCreatePairWithSocket() [IPv6]"]; + goto Failed; + } + } + + // Ensure the CFStream's don't close our underlying socket + + CFReadStreamSetProperty(readStream4, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + CFWriteStreamSetProperty(writeStream4, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + + CFReadStreamSetProperty(readStream6, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + CFWriteStreamSetProperty(writeStream6, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); + + return YES; + +Failed: + if (readStream4) + { + CFReadStreamClose(readStream4); + CFRelease(readStream4); + readStream4 = NULL; + } + if (writeStream4) + { + CFWriteStreamClose(writeStream4); + CFRelease(writeStream4); + writeStream4 = NULL; + } + if (readStream6) + { + CFReadStreamClose(readStream6); + CFRelease(readStream6); + readStream6 = NULL; + } + if (writeStream6) + { + CFWriteStreamClose(writeStream6); + CFRelease(writeStream6); + writeStream6 = NULL; + } + + if (errPtr) + *errPtr = err; + + return NO; +} + +- (BOOL)registerForStreamCallbacks:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); + + NSError *err = nil; + + streamContext.version = 0; + streamContext.info = (__bridge void *)self; + streamContext.retain = nil; + streamContext.release = nil; + streamContext.copyDescription = nil; + + CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + +// readStreamEvents |= (kCFStreamEventOpenCompleted | kCFStreamEventHasBytesAvailable); +// writeStreamEvents |= (kCFStreamEventOpenCompleted | kCFStreamEventCanAcceptBytes); + + if (socket4FD != SOCKET_NULL) + { + if (readStream4 == NULL || writeStream4 == NULL) + { + err = [self otherError:@"Read/Write stream4 is null"]; + goto Failed; + } + + BOOL r1 = CFReadStreamSetClient(readStream4, readStreamEvents, &CFReadStreamCallback, &streamContext); + BOOL r2 = CFWriteStreamSetClient(writeStream4, writeStreamEvents, &CFWriteStreamCallback, &streamContext); + + if (!r1 || !r2) + { + err = [self otherError:@"Error in CFStreamSetClient(), [IPv4]"]; + goto Failed; + } + } + + if (socket6FD != SOCKET_NULL) + { + if (readStream6 == NULL || writeStream6 == NULL) + { + err = [self otherError:@"Read/Write stream6 is null"]; + goto Failed; + } + + BOOL r1 = CFReadStreamSetClient(readStream6, readStreamEvents, &CFReadStreamCallback, &streamContext); + BOOL r2 = CFWriteStreamSetClient(writeStream6, writeStreamEvents, &CFWriteStreamCallback, &streamContext); + + if (!r1 || !r2) + { + err = [self otherError:@"Error in CFStreamSetClient() [IPv6]"]; + goto Failed; + } + } + + return YES; + +Failed: + if (readStream4) { + CFReadStreamSetClient(readStream4, kCFStreamEventNone, NULL, NULL); + } + if (writeStream4) { + CFWriteStreamSetClient(writeStream4, kCFStreamEventNone, NULL, NULL); + } + if (readStream6) { + CFReadStreamSetClient(readStream6, kCFStreamEventNone, NULL, NULL); + } + if (writeStream6) { + CFWriteStreamSetClient(writeStream6, kCFStreamEventNone, NULL, NULL); + } + + if (errPtr) *errPtr = err; + return NO; +} + +- (BOOL)addStreamsToRunLoop:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); + + if (!(flags & kAddedStreamListener)) + { + [[self class] startListenerThreadIfNeeded]; + [[self class] performSelector:@selector(addStreamListener:) + onThread:listenerThread + withObject:self + waitUntilDone:YES]; + + flags |= kAddedStreamListener; + } + + return YES; +} + +- (BOOL)openStreams:(NSError **)errPtr +{ + LogTrace(); + + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); + + NSError *err = nil; + + if (socket4FD != SOCKET_NULL) + { + BOOL r1 = CFReadStreamOpen(readStream4); + BOOL r2 = CFWriteStreamOpen(writeStream4); + + if (!r1 || !r2) + { + err = [self otherError:@"Error in CFStreamOpen() [IPv4]"]; + goto Failed; + } + } + + if (socket6FD != SOCKET_NULL) + { + BOOL r1 = CFReadStreamOpen(readStream6); + BOOL r2 = CFWriteStreamOpen(writeStream6); + + if (!r1 || !r2) + { + err = [self otherError:@"Error in CFStreamOpen() [IPv6]"]; + goto Failed; + } + } + + return YES; + +Failed: + if (errPtr) *errPtr = err; + return NO; +} + +- (void)removeStreamsFromRunLoop +{ + LogTrace(); + NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); + + if (flags & kAddedStreamListener) + { + [[self class] performSelector:@selector(removeStreamListener:) + onThread:listenerThread + withObject:self + waitUntilDone:YES]; + + flags &= ~kAddedStreamListener; + } +} + +- (void)closeReadAndWriteStreams +{ + LogTrace(); + + if (readStream4) + { + CFReadStreamSetClient(readStream4, kCFStreamEventNone, NULL, NULL); + CFReadStreamClose(readStream4); + CFRelease(readStream4); + readStream4 = NULL; + } + if (writeStream4) + { + CFWriteStreamSetClient(writeStream4, kCFStreamEventNone, NULL, NULL); + CFWriteStreamClose(writeStream4); + CFRelease(writeStream4); + writeStream4 = NULL; + } + if (readStream6) + { + CFReadStreamSetClient(readStream6, kCFStreamEventNone, NULL, NULL); + CFReadStreamClose(readStream6); + CFRelease(readStream6); + readStream6 = NULL; + } + if (writeStream6) + { + CFWriteStreamSetClient(writeStream6, kCFStreamEventNone, NULL, NULL); + CFWriteStreamClose(writeStream6); + CFRelease(writeStream6); + writeStream6 = NULL; + } +} + +#endif + +- (void)applicationWillEnterForeground:(NSNotification *)notification +{ + LogTrace(); + + // If the application was backgrounded, then iOS may have shut down our sockets. + // So we take a quick look to see if any of them received an EOF. + + dispatch_block_t block = ^{ @autoreleasepool { + + [self resumeReceive4Source]; + [self resumeReceive6Source]; + }}; + + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_async(socketQueue, block); +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Advanced +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * See header file for big discussion of this method. + **/ +- (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue +{ + void *nonNullUnusedPointer = (__bridge void *)self; + dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); +} + +/** + * See header file for big discussion of this method. + **/ +- (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue +{ + dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL); +} + +- (void)performBlock:(dispatch_block_t)block +{ + if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + block(); + else + dispatch_sync(socketQueue, block); +} + +- (int)socketFD +{ + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return SOCKET_NULL; + } + + if (socket4FD != SOCKET_NULL) + return socket4FD; + else + return socket6FD; +} + +- (int)socket4FD +{ + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return SOCKET_NULL; + } + + return socket4FD; +} + +- (int)socket6FD +{ + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return SOCKET_NULL; + } + + return socket6FD; +} + +#if TARGET_OS_IPHONE + +- (CFReadStreamRef)readStream +{ + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return NULL; + } + + NSError *err = nil; + if (![self createReadAndWriteStreams:&err]) + { + LogError(@"Error creating CFStream(s): %@", err); + return NULL; + } + + // Todo... + + if (readStream4) + return readStream4; + else + return readStream6; +} + +- (CFWriteStreamRef)writeStream +{ + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return NULL; + } + + NSError *err = nil; + if (![self createReadAndWriteStreams:&err]) + { + LogError(@"Error creating CFStream(s): %@", err); + return NULL; + } + + if (writeStream4) + return writeStream4; + else + return writeStream6; +} + +- (BOOL)enableBackgroundingOnSockets +{ + if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) + { + LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", + THIS_FILE, THIS_METHOD); + return NO; + } + + // Why is this commented out? + // See comments below. + +// NSError *err = nil; +// if (![self createReadAndWriteStreams:&err]) +// { +// LogError(@"Error creating CFStream(s): %@", err); +// return NO; +// } +// +// LogVerbose(@"Enabling backgrouding on socket"); +// +// BOOL r1, r2; +// +// if (readStream4 && writeStream4) +// { +// r1 = CFReadStreamSetProperty(readStream4, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); +// r2 = CFWriteStreamSetProperty(writeStream4, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); +// +// if (!r1 || !r2) +// { +// LogError(@"Error setting voip type (IPv4)"); +// return NO; +// } +// } +// +// if (readStream6 && writeStream6) +// { +// r1 = CFReadStreamSetProperty(readStream6, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); +// r2 = CFWriteStreamSetProperty(writeStream6, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); +// +// if (!r1 || !r2) +// { +// LogError(@"Error setting voip type (IPv6)"); +// return NO; +// } +// } +// +// return YES; + + // The above code will actually appear to work. + // The methods will return YES, and everything will appear fine. + // + // One tiny problem: the sockets will still get closed when the app gets backgrounded. + // + // Apple does not officially support backgrounding UDP sockets. + + return NO; +} + +#endif + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Class Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ++ (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + char addrBuf[INET_ADDRSTRLEN]; + + if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + char addrBuf[INET6_ADDRSTRLEN]; + + if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + addrBuf[0] = '\0'; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + ++ (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 +{ + return ntohs(pSockaddr4->sin_port); +} + ++ (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 +{ + return ntohs(pSockaddr6->sin6_port); +} + ++ (NSString *)hostFromAddress:(NSData *)address +{ + NSString *host = nil; + [self getHost:&host port:NULL family:NULL fromAddress:address]; + + return host; +} + ++ (uint16_t)portFromAddress:(NSData *)address +{ + uint16_t port = 0; + [self getHost:NULL port:&port family:NULL fromAddress:address]; + + return port; +} + ++ (int)familyFromAddress:(NSData *)address +{ + int af = AF_UNSPEC; + [self getHost:NULL port:NULL family:&af fromAddress:address]; + + return af; +} + ++ (BOOL)isIPv4Address:(NSData *)address +{ + int af = AF_UNSPEC; + [self getHost:NULL port:NULL family:&af fromAddress:address]; + + return (af == AF_INET); +} + ++ (BOOL)isIPv6Address:(NSData *)address +{ + int af = AF_UNSPEC; + [self getHost:NULL port:NULL family:&af fromAddress:address]; + + return (af == AF_INET6); +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address +{ + return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; +} + ++ (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(int *)afPtr fromAddress:(NSData *)address +{ + if ([address length] >= sizeof(struct sockaddr)) + { + const struct sockaddr *addrX = (const struct sockaddr *)[address bytes]; + + if (addrX->sa_family == AF_INET) + { + if ([address length] >= sizeof(struct sockaddr_in)) + { + const struct sockaddr_in *addr4 = (const struct sockaddr_in *)addrX; + + if (hostPtr) *hostPtr = [self hostFromSockaddr4:addr4]; + if (portPtr) *portPtr = [self portFromSockaddr4:addr4]; + if (afPtr) *afPtr = AF_INET; + + return YES; + } + } + else if (addrX->sa_family == AF_INET6) + { + if ([address length] >= sizeof(struct sockaddr_in6)) + { + const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6 *)addrX; + + if (hostPtr) *hostPtr = [self hostFromSockaddr6:addr6]; + if (portPtr) *portPtr = [self portFromSockaddr6:addr6]; + if (afPtr) *afPtr = AF_INET6; + + return YES; + } + } + } + + if (hostPtr) *hostPtr = nil; + if (portPtr) *portPtr = 0; + if (afPtr) *afPtr = AF_UNSPEC; + + return NO; +} + +@end diff --git a/Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncSocket.h b/Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncSocket.h new file mode 100644 index 0000000..335166b --- /dev/null +++ b/Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncSocket.h @@ -0,0 +1,657 @@ +// +// AsyncSocket.h +// +// This class is in the public domain. +// Originally created by Dustin Voss on Wed Jan 29 2003. +// Updated and maintained by Deusty Designs and the Mac development community. +// +// http://code.google.com/p/cocoaasyncsocket/ +// + +#import + +@class AsyncSocket; +@class AsyncReadPacket; +@class AsyncWritePacket; + +extern NSString *const AsyncSocketException; +extern NSString *const AsyncSocketErrorDomain; + +typedef NS_ENUM(NSInteger, AsyncSocketError) { + AsyncSocketCFSocketError = kCFSocketError, // From CFSocketError enum. + AsyncSocketNoError = 0, // Never used. + AsyncSocketCanceledError, // onSocketWillConnect: returned NO. + AsyncSocketConnectTimeoutError, + AsyncSocketReadMaxedOutError, // Reached set maxLength without completing + AsyncSocketReadTimeoutError, + AsyncSocketWriteTimeoutError +}; + +@protocol AsyncSocketDelegate +@optional + +/** + * In the event of an error, the socket is closed. + * You may call "unreadData" during this call-back to get the last bit of data off the socket. + * When connecting, this delegate method may be called + * before"onSocket:didAcceptNewSocket:" or "onSocket:didConnectToHost:". +**/ +- (void)onSocket:(AsyncSocket *)sock willDisconnectWithError:(NSError *)err; + +/** + * Called when a socket disconnects with or without error. If you want to release a socket after it disconnects, + * do so here. It is not safe to do that during "onSocket:willDisconnectWithError:". + * + * If you call the disconnect method, and the socket wasn't already disconnected, + * this delegate method will be called before the disconnect method returns. +**/ +- (void)onSocketDidDisconnect:(AsyncSocket *)sock; + +/** + * Called when a socket accepts a connection. Another socket is spawned to handle it. The new socket will have + * the same delegate and will call "onSocket:didConnectToHost:port:". +**/ +- (void)onSocket:(AsyncSocket *)sock didAcceptNewSocket:(AsyncSocket *)newSocket; + +/** + * Called when a new socket is spawned to handle a connection. This method should return the run-loop of the + * thread on which the new socket and its delegate should operate. If omitted, [NSRunLoop currentRunLoop] is used. +**/ +- (NSRunLoop *)onSocket:(AsyncSocket *)sock wantsRunLoopForNewSocket:(AsyncSocket *)newSocket; + +/** + * Called when a socket is about to connect. This method should return YES to continue, or NO to abort. + * If aborted, will result in AsyncSocketCanceledError. + * + * If the connectToHost:onPort:error: method was called, the delegate will be able to access and configure the + * CFReadStream and CFWriteStream as desired prior to connection. + * + * If the connectToAddress:error: method was called, the delegate will be able to access and configure the + * CFSocket and CFSocketNativeHandle (BSD socket) as desired prior to connection. You will be able to access and + * configure the CFReadStream and CFWriteStream in the onSocket:didConnectToHost:port: method. +**/ +- (BOOL)onSocketWillConnect:(AsyncSocket *)sock; + +/** + * Called when a socket connects and is ready for reading and writing. + * The host parameter will be an IP address, not a DNS name. +**/ +- (void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port; + +/** + * Called when a socket has completed reading the requested data into memory. + * Not called if there is an error. +**/ +- (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag; + +/** + * Called when a socket has read in data, but has not yet completed the read. + * This would occur if using readToData: or readToLength: methods. + * It may be used to for things such as updating progress bars. +**/ +- (void)onSocket:(AsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called when a socket has completed writing the requested data. Not called if there is an error. +**/ +- (void)onSocket:(AsyncSocket *)sock didWriteDataWithTag:(long)tag; + +/** + * Called when a socket has written some data, but has not yet completed the entire write. + * It may be used to for things such as updating progress bars. +**/ +- (void)onSocket:(AsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + * Called if a read operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been read so far for the read operation. + * + * Note that this method may be called multiple times for a single read if you return positive numbers. +**/ +- (NSTimeInterval)onSocket:(AsyncSocket *)sock + shouldTimeoutReadWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Called if a write operation has reached its timeout without completing. + * This method allows you to optionally extend the timeout. + * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount. + * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual. + * + * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. + * The length parameter is the number of bytes that have been written so far for the write operation. + * + * Note that this method may be called multiple times for a single write if you return positive numbers. +**/ +- (NSTimeInterval)onSocket:(AsyncSocket *)sock + shouldTimeoutWriteWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + * Called after the socket has successfully completed SSL/TLS negotiation. + * This method is not called unless you use the provided startTLS method. + * + * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close, + * and the onSocket:willDisconnectWithError: delegate method will be called with the specific SSL error code. +**/ +- (void)onSocketDidSecure:(AsyncSocket *)sock; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@interface AsyncSocket : NSObject +{ + CFSocketNativeHandle theNativeSocket4; + CFSocketNativeHandle theNativeSocket6; + + CFSocketRef theSocket4; // IPv4 accept or connect socket + CFSocketRef theSocket6; // IPv6 accept or connect socket + + CFReadStreamRef theReadStream; + CFWriteStreamRef theWriteStream; + + CFRunLoopSourceRef theSource4; // For theSocket4 + CFRunLoopSourceRef theSource6; // For theSocket6 + CFRunLoopRef theRunLoop; + CFSocketContext theContext; + NSArray *theRunLoopModes; + + NSTimer *theConnectTimer; + + NSMutableArray *theReadQueue; + AsyncReadPacket *theCurrentRead; + NSTimer *theReadTimer; + NSMutableData *partialReadBuffer; + + NSMutableArray *theWriteQueue; + AsyncWritePacket *theCurrentWrite; + NSTimer *theWriteTimer; + + id theDelegate; + UInt16 theFlags; + + long theUserData; +} + +- (id)init; +- (id)initWithDelegate:(id)delegate; +- (id)initWithDelegate:(id)delegate userData:(long)userData; + +/* String representation is long but has no "\n". */ +- (NSString *)description; + +/** + * Use "canSafelySetDelegate" to see if there is any pending business (reads and writes) with the current delegate + * before changing it. It is, of course, safe to change the delegate before connecting or accepting connections. +**/ +- (id)delegate; +- (BOOL)canSafelySetDelegate; +- (void)setDelegate:(id)delegate; + +/* User data can be a long, or an id or void * cast to a long. */ +- (long)userData; +- (void)setUserData:(long)userData; + +/* Don't use these to read or write. And don't close them either! */ +- (CFSocketRef)getCFSocket; +- (CFReadStreamRef)getCFReadStream; +- (CFWriteStreamRef)getCFWriteStream; + +// Once one of the accept or connect methods are called, the AsyncSocket instance is locked in +// and the other accept/connect methods can't be called without disconnecting the socket first. +// If the attempt fails or times out, these methods either return NO or +// call "onSocket:willDisconnectWithError:" and "onSockedDidDisconnect:". + +// When an incoming connection is accepted, AsyncSocket invokes several delegate methods. +// These methods are (in chronological order): +// 1. onSocket:didAcceptNewSocket: +// 2. onSocket:wantsRunLoopForNewSocket: +// 3. onSocketWillConnect: +// +// Your server code will need to retain the accepted socket (if you want to accept it). +// The best place to do this is probably in the onSocket:didAcceptNewSocket: method. +// +// After the read and write streams have been setup for the newly accepted socket, +// the onSocket:didConnectToHost:port: method will be called on the proper run loop. +// +// Multithreading Note: If you're going to be moving the newly accepted socket to another run +// loop by implementing onSocket:wantsRunLoopForNewSocket:, then you should wait until the +// onSocket:didConnectToHost:port: method before calling read, write, or startTLS methods. +// Otherwise read/write events are scheduled on the incorrect runloop, and chaos may ensue. + +/** + * Tells the socket to begin listening and accepting connections on the given port. + * When a connection comes in, the AsyncSocket instance will call the various delegate methods (see above). + * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) +**/ +- (BOOL)acceptOnPort:(UInt16)port error:(NSError **)errPtr; + +/** + * This method is the same as acceptOnPort:error: with the additional option + * of specifying which interface to listen on. So, for example, if you were writing code for a server that + * has multiple IP addresses, you could specify which address you wanted to listen on. Or you could use it + * to specify that the socket should only accept connections over ethernet, and not other interfaces such as wifi. + * You may also use the special strings "localhost" or "loopback" to specify that + * the socket only accept connections from the local machine. + * + * To accept connections on any interface pass nil, or simply use the acceptOnPort:error: method. +**/ +- (BOOL)acceptOnInterface:(NSString *)interface port:(UInt16)port error:(NSError **)errPtr; + +/** + * Connects to the given host and port. + * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2") +**/ +- (BOOL)connectToHost:(NSString *)hostname onPort:(UInt16)port error:(NSError **)errPtr; + +/** + * This method is the same as connectToHost:onPort:error: with an additional timeout option. + * To not time out use a negative time interval, or simply use the connectToHost:onPort:error: method. +**/ +- (BOOL)connectToHost:(NSString *)hostname + onPort:(UInt16)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Connects to the given address, specified as a sockaddr structure wrapped in a NSData object. + * For example, a NSData object returned from NSNetService's addresses method. + * + * If you have an existing struct sockaddr you can convert it to a NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +/** + * This method is the same as connectToAddress:error: with an additional timeout option. + * To not time out use a negative time interval, or simply use the connectToAddress:error: method. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; + +- (BOOL)connectToAddress:(NSData *)remoteAddr + viaInterfaceAddress:(NSData *)interfaceAddr + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr; + +/** + * Disconnects immediately. Any pending reads or writes are dropped. + * If the socket is not already disconnected, the onSocketDidDisconnect delegate method + * will be called immediately, before this method returns. + * + * Please note the recommended way of releasing an AsyncSocket instance (e.g. in a dealloc method) + * [asyncSocket setDelegate:nil]; + * [asyncSocket disconnect]; + * [asyncSocket release]; +**/ +- (void)disconnect; + +/** + * Disconnects after all pending reads have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending writes. +**/ +- (void)disconnectAfterReading; + +/** + * Disconnects after all pending writes have completed. + * After calling this, the read and write methods will do nothing. + * The socket will disconnect even if there are still pending reads. +**/ +- (void)disconnectAfterWriting; + +/** + * Disconnects after all pending reads and writes have completed. + * After calling this, the read and write methods will do nothing. +**/ +- (void)disconnectAfterReadingAndWriting; + +/* Returns YES if the socket and streams are open, connected, and ready for reading and writing. */ +- (BOOL)isConnected; + +/** + * Returns the local or remote host and port to which this socket is connected, or nil and 0 if not connected. + * The host will be an IP address. +**/ +- (NSString *)connectedHost; +- (UInt16)connectedPort; + +- (NSString *)localHost; +- (UInt16)localPort; + +/** + * Returns the local or remote address to which this socket is connected, + * specified as a sockaddr structure wrapped in a NSData object. + * + * See also the connectedHost, connectedPort, localHost and localPort methods. +**/ +- (NSData *)connectedAddress; +- (NSData *)localAddress; + +/** + * Returns whether the socket is IPv4 or IPv6. + * An accepting socket may be both. +**/ +- (BOOL)isIPv4; +- (BOOL)isIPv6; + +// The readData and writeData methods won't block (they are asynchronous). +// +// When a read is complete the onSocket:didReadData:withTag: delegate method is called. +// When a write is complete the onSocket:didWriteDataWithTag: delegate method is called. +// +// You may optionally set a timeout for any read/write operation. (To not timeout, use a negative time interval.) +// If a read/write opertion times out, the corresponding "onSocket:shouldTimeout..." delegate method +// is called to optionally allow you to extend the timeout. +// Upon a timeout, the "onSocket:willDisconnectWithError:" method is called, followed by "onSocketDidDisconnect". +// +// The tag is for your convenience. +// You can use it as an array index, step number, state id, pointer, etc. + +/** + * Reads the first available bytes that become available on the socket. + * + * If the timeout value is negative, the read operation will not use a timeout. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, the socket will create a buffer for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. + * After completion, the data returned in onSocket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads the first available bytes that become available on the socket. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * A maximum of length bytes will be read. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * If maxLength is zero, no length restriction is enforced. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. + * After completion, the data returned in onSocket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer. +**/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Reads the given number of bytes. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If the length is 0, this method does nothing and the delegate is not called. +**/ +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads the given number of bytes. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If the length is 0, this method does nothing and the delegate is not called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. + * After completion, the data returned in onSocket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer. +**/ +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing, and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * Note that this method is not character-set aware, so if a separator can occur naturally as part of the encoding for + * a character, the read will prematurely end. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. + * After completion, the data returned in onSocket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * Note that this method is not character-set aware, so if a separator can occur naturally as part of the encoding for + * a character, the read will prematurely end. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * + * If the timeout value is negative, the read operation will not use a timeout. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a AsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass nil or zero-length data as the "data" parameter, + * the method will do nothing, and the delegate will not be called. + * If you pass a maxLength parameter that is less than the length of the data parameter, + * the method will do nothing, and the delegate will not be called. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * Note that this method is not character-set aware, so if a separator can occur naturally as part of the encoding for + * a character, the read will prematurely end. +**/ +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag; + +/** + * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. + * The bytes will be appended to the given byte buffer starting at the given offset. + * The given buffer will automatically be increased in size if needed. + * A maximum of length bytes will be read. + * + * If the timeout value is negative, the read operation will not use a timeout. + * If the buffer if nil, a buffer will automatically be created for you. + * + * If maxLength is zero, no length restriction is enforced. + * Otherwise if maxLength bytes are read without completing the read, + * it is treated similarly to a timeout - the socket is closed with a AsyncSocketReadMaxedOutError. + * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. + * + * If you pass a maxLength parameter that is less than the length of the data parameter, + * the method will do nothing, and the delegate will not be called. + * If the bufferOffset is greater than the length of the given buffer, + * the method will do nothing, and the delegate will not be called. + * + * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. + * After completion, the data returned in onSocket:didReadData:withTag: will be a subset of the given buffer. + * That is, it will reference the bytes that were appended to the given buffer. + * + * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. + * Note that this method is not character-set aware, so if a separator can occur naturally as part of the encoding for + * a character, the read will prematurely end. +**/ +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + * Writes data to the socket, and calls the delegate when finished. + * + * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called. + * If the timeout value is negative, the write operation will not use a timeout. +**/ +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Returns progress of current read or write, from 0.0 to 1.0, or NaN if no read/write (use isnan() to check). + * "tag", "done" and "total" will be filled in if they aren't NULL. +**/ +- (float)progressOfReadReturningTag:(long *)tag bytesDone:(NSUInteger *)done total:(NSUInteger *)total; +- (float)progressOfWriteReturningTag:(long *)tag bytesDone:(NSUInteger *)done total:(NSUInteger *)total; + +/** + * Secures the connection using SSL/TLS. + * + * This method may be called at any time, and the TLS handshake will occur after all pending reads and writes + * are finished. This allows one the option of sending a protocol dependent StartTLS message, and queuing + * the upgrade to TLS at the same time, without having to wait for the write to finish. + * Any reads or writes scheduled after this method is called will occur over the secured connection. + * + * The possible keys and values for the TLS settings are well documented. + * Some possible keys are: + * - kCFStreamSSLLevel + * - kCFStreamSSLAllowsExpiredCertificates + * - kCFStreamSSLAllowsExpiredRoots + * - kCFStreamSSLAllowsAnyRoot + * - kCFStreamSSLValidatesCertificateChain + * - kCFStreamSSLPeerName + * - kCFStreamSSLCertificates + * - kCFStreamSSLIsServer + * + * Please refer to Apple's documentation for associated values, as well as other possible keys. + * + * If you pass in nil or an empty dictionary, the default settings will be used. + * + * The default settings will check to make sure the remote party's certificate is signed by a + * trusted 3rd party certificate agency (e.g. verisign) and that the certificate is not expired. + * However it will not verify the name on the certificate unless you + * give it a name to verify against via the kCFStreamSSLPeerName key. + * The security implications of this are important to understand. + * Imagine you are attempting to create a secure connection to MySecureServer.com, + * but your socket gets directed to MaliciousServer.com because of a hacked DNS server. + * If you simply use the default settings, and MaliciousServer.com has a valid certificate, + * the default settings will not detect any problems since the certificate is valid. + * To properly secure your connection in this particular scenario you + * should set the kCFStreamSSLPeerName property to "MySecureServer.com". + * If you do not know the peer name of the remote host in advance (for example, you're not sure + * if it will be "domain.com" or "www.domain.com"), then you can use the default settings to validate the + * certificate, and then use the X509Certificate class to verify the issuer after the socket has been secured. + * The X509Certificate class is part of the CocoaAsyncSocket open source project. +**/ +- (void)startTLS:(NSDictionary *)tlsSettings; + +/** + * For handling readDataToData requests, data is necessarily read from the socket in small increments. + * The performance can be much improved by allowing AsyncSocket to read larger chunks at a time and + * store any overflow in a small internal buffer. + * This is termed pre-buffering, as some data may be read for you before you ask for it. + * If you use readDataToData a lot, enabling pre-buffering will result in better performance, especially on the iPhone. + * + * The default pre-buffering state is controlled by the DEFAULT_PREBUFFERING definition. + * It is highly recommended one leave this set to YES. + * + * This method exists in case pre-buffering needs to be disabled by default for some unforeseen reason. + * In that case, this method exists to allow one to easily enable pre-buffering when ready. +**/ +- (void)enablePreBuffering; + +/** + * When you create an AsyncSocket, it is added to the runloop of the current thread. + * So for manually created sockets, it is easiest to simply create the socket on the thread you intend to use it. + * + * If a new socket is accepted, the delegate method onSocket:wantsRunLoopForNewSocket: is called to + * allow you to place the socket on a separate thread. This works best in conjunction with a thread pool design. + * + * If, however, you need to move the socket to a separate thread at a later time, this + * method may be used to accomplish the task. + * + * This method must be called from the thread/runloop the socket is currently running on. + * + * Note: After calling this method, all further method calls to this object should be done from the given runloop. + * Also, all delegate calls will be sent on the given runloop. +**/ +- (BOOL)moveToRunLoop:(NSRunLoop *)runLoop; + +/** + * Allows you to configure which run loop modes the socket uses. + * The default set of run loop modes is NSDefaultRunLoopMode. + * + * If you'd like your socket to continue operation during other modes, you may want to add modes such as + * NSModalPanelRunLoopMode or NSEventTrackingRunLoopMode. Or you may simply want to use NSRunLoopCommonModes. + * + * Accepted sockets will automatically inherit the same run loop modes as the listening socket. + * + * Note: NSRunLoopCommonModes is defined in 10.5. For previous versions one can use kCFRunLoopCommonModes. +**/ +- (BOOL)setRunLoopModes:(NSArray *)runLoopModes; +- (BOOL)addRunLoopMode:(NSString *)runLoopMode; +- (BOOL)removeRunLoopMode:(NSString *)runLoopMode; + +/** + * Returns the current run loop modes the AsyncSocket instance is operating in. + * The default set of run loop modes is NSDefaultRunLoopMode. +**/ +- (NSArray *)runLoopModes; + +/** + * In the event of an error, this method may be called during onSocket:willDisconnectWithError: to read + * any data that's left on the socket. +**/ +- (NSData *)unreadData; + +/* A few common line separators, for use with the readDataToData:... methods. */ ++ (NSData *)CRLFData; // 0x0D0A ++ (NSData *)CRData; // 0x0D ++ (NSData *)LFData; // 0x0A ++ (NSData *)ZeroData; // 0x00 + +@end diff --git a/Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncSocket.m b/Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncSocket.m new file mode 100644 index 0000000..8931679 --- /dev/null +++ b/Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncSocket.m @@ -0,0 +1,4313 @@ +// +// AsyncSocket.m +// +// This class is in the public domain. +// Originally created by Dustin Voss on Wed Jan 29 2003. +// Updated and maintained by Deusty Designs and the Mac development community. +// +// http://code.google.com/p/cocoaasyncsocket/ +// + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#import "AsyncSocket.h" +#import +#import +#import +#import +#import + +#if TARGET_OS_IPHONE +// Note: You may need to add the CFNetwork Framework to your project +#import +#endif + +#pragma mark Declarations + +#define DEFAULT_PREBUFFERING YES // Whether pre-buffering is enabled by default + +#define READQUEUE_CAPACITY 5 // Initial capacity +#define WRITEQUEUE_CAPACITY 5 // Initial capacity +#define READALL_CHUNKSIZE 256 // Incremental increase in buffer size +#define WRITE_CHUNKSIZE (1024 * 4) // Limit on size of each write pass + +// AsyncSocket is RunLoop based, and is thus not thread-safe. +// You must always access your AsyncSocket instance from the thread/runloop in which the instance is running. +// You can use methods such as performSelectorOnThread to accomplish this. +// Failure to comply with these thread-safety rules may result in errors. +// You can enable this option to help diagnose where you are incorrectly accessing your socket. +#if DEBUG + #define DEBUG_THREAD_SAFETY 1 +#else + #define DEBUG_THREAD_SAFETY 0 +#endif +// +// If you constantly need to access your socket from multiple threads +// then you may consider using GCDAsyncSocket instead, which is thread-safe. + +NSString *const AsyncSocketException = @"AsyncSocketException"; +NSString *const AsyncSocketErrorDomain = @"AsyncSocketErrorDomain"; + + +enum AsyncSocketFlags +{ + kEnablePreBuffering = 1 << 0, // If set, pre-buffering is enabled + kDidStartDelegate = 1 << 1, // If set, disconnection results in delegate call + kDidCompleteOpenForRead = 1 << 2, // If set, open callback has been called for read stream + kDidCompleteOpenForWrite = 1 << 3, // If set, open callback has been called for write stream + kStartingReadTLS = 1 << 4, // If set, we're waiting for TLS negotiation to complete + kStartingWriteTLS = 1 << 5, // If set, we're waiting for TLS negotiation to complete + kForbidReadsWrites = 1 << 6, // If set, no new reads or writes are allowed + kDisconnectAfterReads = 1 << 7, // If set, disconnect after no more reads are queued + kDisconnectAfterWrites = 1 << 8, // If set, disconnect after no more writes are queued + kClosingWithError = 1 << 9, // If set, the socket is being closed due to an error + kDequeueReadScheduled = 1 << 10, // If set, a maybeDequeueRead operation is already scheduled + kDequeueWriteScheduled = 1 << 11, // If set, a maybeDequeueWrite operation is already scheduled + kSocketCanAcceptBytes = 1 << 12, // If set, we know socket can accept bytes. If unset, it's unknown. + kSocketHasBytesAvailable = 1 << 13, // If set, we know socket has bytes available. If unset, it's unknown. +}; + +@interface AsyncSocket (Private) + +// Connecting +- (void)startConnectTimeout:(NSTimeInterval)timeout; +- (void)endConnectTimeout; +- (void)doConnectTimeout:(NSTimer *)timer; + +// Socket Implementation +- (CFSocketRef)newAcceptSocketForAddress:(NSData *)addr error:(NSError **)errPtr; +- (BOOL)createSocketForAddress:(NSData *)remoteAddr error:(NSError **)errPtr; +- (BOOL)bindSocketToAddress:(NSData *)interfaceAddr error:(NSError **)errPtr; +- (BOOL)attachSocketsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr; +- (BOOL)configureSocketAndReturnError:(NSError **)errPtr; +- (BOOL)connectSocketToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; +- (void)doAcceptWithSocket:(CFSocketNativeHandle)newSocket; +- (void)doSocketOpen:(CFSocketRef)sock withCFSocketError:(CFSocketError)err; + +// Stream Implementation +- (BOOL)createStreamsFromNative:(CFSocketNativeHandle)native error:(NSError **)errPtr; +- (BOOL)createStreamsToHost:(NSString *)hostname onPort:(UInt16)port error:(NSError **)errPtr; +- (BOOL)attachStreamsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr; +- (BOOL)configureStreamsAndReturnError:(NSError **)errPtr; +- (BOOL)openStreamsAndReturnError:(NSError **)errPtr; +- (void)doStreamOpen; +- (BOOL)setSocketFromStreamsAndReturnError:(NSError **)errPtr; + +// Disconnect Implementation +- (void)closeWithError:(NSError *)err; +- (void)recoverUnreadData; +- (void)emptyQueues; +- (void)close; + +// Errors +- (NSError *)getErrnoError; +- (NSError *)getAbortError; +- (NSError *)getStreamError; +- (NSError *)getSocketError; +- (NSError *)getConnectTimeoutError; +- (NSError *)getReadMaxedOutError; +- (NSError *)getReadTimeoutError; +- (NSError *)getWriteTimeoutError; +- (NSError *)errorFromCFStreamError:(CFStreamError)err; + +// Diagnostics +- (BOOL)isDisconnected; +- (BOOL)areStreamsConnected; +- (NSString *)connectedHostFromNativeSocket4:(CFSocketNativeHandle)theNativeSocket; +- (NSString *)connectedHostFromNativeSocket6:(CFSocketNativeHandle)theNativeSocket; +- (NSString *)connectedHostFromCFSocket4:(CFSocketRef)socket; +- (NSString *)connectedHostFromCFSocket6:(CFSocketRef)socket; +- (UInt16)connectedPortFromNativeSocket4:(CFSocketNativeHandle)theNativeSocket; +- (UInt16)connectedPortFromNativeSocket6:(CFSocketNativeHandle)theNativeSocket; +- (UInt16)connectedPortFromCFSocket4:(CFSocketRef)socket; +- (UInt16)connectedPortFromCFSocket6:(CFSocketRef)socket; +- (NSString *)localHostFromNativeSocket4:(CFSocketNativeHandle)theNativeSocket; +- (NSString *)localHostFromNativeSocket6:(CFSocketNativeHandle)theNativeSocket; +- (NSString *)localHostFromCFSocket4:(CFSocketRef)socket; +- (NSString *)localHostFromCFSocket6:(CFSocketRef)socket; +- (UInt16)localPortFromNativeSocket4:(CFSocketNativeHandle)theNativeSocket; +- (UInt16)localPortFromNativeSocket6:(CFSocketNativeHandle)theNativeSocket; +- (UInt16)localPortFromCFSocket4:(CFSocketRef)socket; +- (UInt16)localPortFromCFSocket6:(CFSocketRef)socket; +- (NSString *)hostFromAddress4:(struct sockaddr_in *)pSockaddr4; +- (NSString *)hostFromAddress6:(struct sockaddr_in6 *)pSockaddr6; +- (UInt16)portFromAddress4:(struct sockaddr_in *)pSockaddr4; +- (UInt16)portFromAddress6:(struct sockaddr_in6 *)pSockaddr6; + +// Reading +- (void)doBytesAvailable; +- (void)completeCurrentRead; +- (void)endCurrentRead; +- (void)scheduleDequeueRead; +- (void)maybeDequeueRead; +- (void)doReadTimeout:(NSTimer *)timer; + +// Writing +- (void)doSendBytes; +- (void)completeCurrentWrite; +- (void)endCurrentWrite; +- (void)scheduleDequeueWrite; +- (void)maybeDequeueWrite; +- (void)maybeScheduleDisconnect; +- (void)doWriteTimeout:(NSTimer *)timer; + +// Run Loop +- (void)runLoopAddSource:(CFRunLoopSourceRef)source; +- (void)runLoopRemoveSource:(CFRunLoopSourceRef)source; +- (void)runLoopAddTimer:(NSTimer *)timer; +- (void)runLoopRemoveTimer:(NSTimer *)timer; +- (void)runLoopUnscheduleReadStream; +- (void)runLoopUnscheduleWriteStream; + +// Security +- (void)maybeStartTLS; +- (void)onTLSHandshakeSuccessful; + +// Callbacks +- (void)doCFCallback:(CFSocketCallBackType)type + forSocket:(CFSocketRef)sock withAddress:(NSData *)address withData:(const void *)pData; +- (void)doCFReadStreamCallback:(CFStreamEventType)type forStream:(CFReadStreamRef)stream; +- (void)doCFWriteStreamCallback:(CFStreamEventType)type forStream:(CFWriteStreamRef)stream; + +@end + +static void MyCFSocketCallback(CFSocketRef, CFSocketCallBackType, CFDataRef, const void *, void *); +static void MyCFReadStreamCallback(CFReadStreamRef stream, CFStreamEventType type, void *pInfo); +static void MyCFWriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType type, void *pInfo); + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The AsyncReadPacket encompasses the instructions for any given read. + * The content of a read packet allows the code to determine if we're: + * - reading to a certain length + * - reading to a certain separator + * - or simply reading the first chunk of available data +**/ +@interface AsyncReadPacket : NSObject +{ + @public + NSMutableData *buffer; + NSUInteger startOffset; + NSUInteger bytesDone; + NSUInteger maxLength; + NSTimeInterval timeout; + NSUInteger readLength; + NSData *term; + BOOL bufferOwner; + NSUInteger originalBufferLength; + long tag; +} +- (id)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i; + +- (NSUInteger)readLengthForNonTerm; +- (NSUInteger)readLengthForTerm; +- (NSUInteger)readLengthForTermWithPreBuffer:(NSData *)preBuffer found:(BOOL *)foundPtr; + +- (NSUInteger)prebufferReadLengthForTerm; +- (NSInteger)searchForTermAfterPreBuffering:(NSUInteger)numBytes; +@end + +@implementation AsyncReadPacket + +- (id)initWithData:(NSMutableData *)d + startOffset:(NSUInteger)s + maxLength:(NSUInteger)m + timeout:(NSTimeInterval)t + readLength:(NSUInteger)l + terminator:(NSData *)e + tag:(long)i +{ + if((self = [super init])) + { + if (d) + { + buffer = d; + startOffset = s; + bufferOwner = NO; + originalBufferLength = [d length]; + } + else + { + if (readLength > 0) + buffer = [[NSMutableData alloc] initWithLength:readLength]; + else + buffer = [[NSMutableData alloc] initWithLength:0]; + + startOffset = 0; + bufferOwner = YES; + originalBufferLength = 0; + } + + bytesDone = 0; + maxLength = m; + timeout = t; + readLength = l; + term = [e copy]; + tag = i; + } + return self; +} + +/** + * For read packets without a set terminator, returns the safe length of data that can be read + * without exceeding the maxLength, or forcing a resize of the buffer if at all possible. +**/ +- (NSUInteger)readLengthForNonTerm +{ + NSAssert(term == nil, @"This method does not apply to term reads"); + + if (readLength > 0) + { + // Read a specific length of data + + return readLength - bytesDone; + + // No need to avoid resizing the buffer. + // It should be resized if the buffer space is less than the requested read length. + } + else + { + // Read all available data + + NSUInteger result = READALL_CHUNKSIZE; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + if (!bufferOwner) + { + // We did NOT create the buffer. + // It is owned by the caller. + // Avoid resizing the buffer if at all possible. + + if ([buffer length] == originalBufferLength) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffSpace = buffSize - startOffset - bytesDone; + + if (buffSpace > 0) + { + result = MIN(result, buffSpace); + } + } + } + + return result; + } +} + +/** + * For read packets with a set terminator, returns the safe length of data that can be read + * without going over a terminator, or the maxLength, or forcing a resize of the buffer if at all possible. + * + * It is assumed the terminator has not already been read. +**/ +- (NSUInteger)readLengthForTerm +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + + // What we're going to do is look for a partial sequence of the terminator at the end of the buffer. + // If a partial sequence occurs, then we must assume the next bytes to arrive will be the rest of the term, + // and we can only read that amount. + // Otherwise, we're safe to read the entire length of the term. + + NSUInteger termLength = [term length]; + + // Shortcuts + if (bytesDone == 0) return termLength; + if (termLength == 1) return termLength; + + // i = index within buffer at which to check data + // j = length of term to check against + + NSUInteger i, j; + if (bytesDone >= termLength) + { + i = bytesDone - termLength + 1; + j = termLength - 1; + } + else + { + i = 0; + j = bytesDone; + } + + NSUInteger result = termLength; + + void *buf = [buffer mutableBytes]; + const void *termBuf = [term bytes]; + + while (i < bytesDone) + { + void *subbuf = buf + startOffset + i; + + if (memcmp(subbuf, termBuf, j) == 0) + { + result = termLength - j; + break; + } + + i++; + j--; + } + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + if (!bufferOwner) + { + // We did NOT create the buffer. + // It is owned by the caller. + // Avoid resizing the buffer if at all possible. + + if ([buffer length] == originalBufferLength) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffSpace = buffSize - startOffset - bytesDone; + + if (buffSpace > 0) + { + result = MIN(result, buffSpace); + } + } + } + + return result; +} + +/** + * For read packets with a set terminator, + * returns the safe length of data that can be read from the given preBuffer, + * without going over a terminator or the maxLength. + * + * It is assumed the terminator has not already been read. +**/ +- (NSUInteger)readLengthForTermWithPreBuffer:(NSData *)preBuffer found:(BOOL *)foundPtr +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert([preBuffer length] > 0, @"Invoked with empty pre buffer!"); + + // We know that the terminator, as a whole, doesn't exist in our own buffer. + // But it is possible that a portion of it exists in our buffer. + // So we're going to look for the terminator starting with a portion of our own buffer. + // + // Example: + // + // term length = 3 bytes + // bytesDone = 5 bytes + // preBuffer length = 5 bytes + // + // If we append the preBuffer to our buffer, + // it would look like this: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // --------------------- + // + // So we start our search here: + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // -------^-^-^--------- + // + // And move forwards... + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------^-^-^------- + // + // Until we find the terminator or reach the end. + // + // --------------------- + // |B|B|B|B|B|P|P|P|P|P| + // ---------------^-^-^- + + BOOL found = NO; + + NSUInteger termLength = [term length]; + NSUInteger preBufferLength = [preBuffer length]; + + if ((bytesDone + preBufferLength) < termLength) + { + // Not enough data for a full term sequence yet + return preBufferLength; + } + + NSUInteger maxPreBufferLength; + if (maxLength > 0) { + maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone)); + + // Note: maxLength >= termLength + } + else { + maxPreBufferLength = preBufferLength; + } + + Byte seq[termLength]; + const void *termBuf = [term bytes]; + + NSUInteger bufLen = MIN(bytesDone, (termLength - 1)); + void *buf = [buffer mutableBytes] + startOffset + bytesDone - bufLen; + + NSUInteger preLen = termLength - bufLen; + void *pre = (void *)[preBuffer bytes]; + + NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above. + + NSUInteger result = preBufferLength; + + NSUInteger i; + for (i = 0; i < loopCount; i++) + { + if (bufLen > 0) + { + // Combining bytes from buffer and preBuffer + + memcpy(seq, buf, bufLen); + memcpy(seq + bufLen, pre, preLen); + + if (memcmp(seq, termBuf, termLength) == 0) + { + result = preLen; + found = YES; + break; + } + + buf++; + bufLen--; + preLen++; + } + else + { + // Comparing directly from preBuffer + + if (memcmp(pre, termBuf, termLength) == 0) + { + NSUInteger preOffset = pre - [preBuffer bytes]; // pointer arithmetic + + result = preOffset + termLength; + found = YES; + break; + } + + pre++; + } + } + + // There is no need to avoid resizing the buffer in this particular situation. + + if (foundPtr) *foundPtr = found; + return result; +} + +/** + * Assuming pre-buffering is enabled, returns the amount of data that can be read + * without going over the maxLength. +**/ +- (NSUInteger)prebufferReadLengthForTerm +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + + NSUInteger result = READALL_CHUNKSIZE; + + if (maxLength > 0) + { + result = MIN(result, (maxLength - bytesDone)); + } + + if (!bufferOwner) + { + // We did NOT create the buffer. + // It is owned by the caller. + // Avoid resizing the buffer if at all possible. + + if ([buffer length] == originalBufferLength) + { + NSUInteger buffSize = [buffer length]; + NSUInteger buffSpace = buffSize - startOffset - bytesDone; + + if (buffSpace > 0) + { + result = MIN(result, buffSpace); + } + } + } + + return result; +} + +/** + * For read packets with a set terminator, scans the packet buffer for the term. + * It is assumed the terminator had not been fully read prior to the new bytes. + * + * If the term is found, the number of excess bytes after the term are returned. + * If the term is not found, this method will return -1. + * + * Note: A return value of zero means the term was found at the very end. +**/ +- (NSInteger)searchForTermAfterPreBuffering:(NSUInteger)numBytes +{ + NSAssert(term != nil, @"This method does not apply to non-term reads"); + NSAssert(bytesDone >= numBytes, @"Invoked with invalid numBytes!"); + + // We try to start the search such that the first new byte read matches up with the last byte of the term. + // We continue searching forward after this until the term no longer fits into the buffer. + + NSUInteger termLength = [term length]; + const void *termBuffer = [term bytes]; + + // Remember: This method is called after the bytesDone variable has been updated. + + NSUInteger prevBytesDone = bytesDone - numBytes; + + NSUInteger i; + if (prevBytesDone >= termLength) + i = prevBytesDone - termLength + 1; + else + i = 0; + + while ((i + termLength) <= bytesDone) + { + void *subBuffer = [buffer mutableBytes] + startOffset + i; + + if(memcmp(subBuffer, termBuffer, termLength) == 0) + { + return bytesDone - (i + termLength); + } + + i++; + } + + return -1; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The AsyncWritePacket encompasses the instructions for any given write. +**/ +@interface AsyncWritePacket : NSObject +{ + @public + NSData *buffer; + NSUInteger bytesDone; + long tag; + NSTimeInterval timeout; +} +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i; +@end + +@implementation AsyncWritePacket + +- (id)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i +{ + if((self = [super init])) + { + buffer = d; + timeout = t; + tag = i; + bytesDone = 0; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The AsyncSpecialPacket encompasses special instructions for interruptions in the read/write queues. + * This class my be altered to support more than just TLS in the future. +**/ +@interface AsyncSpecialPacket : NSObject +{ + @public + NSDictionary *tlsSettings; +} +- (id)initWithTLSSettings:(NSDictionary *)settings; +@end + +@implementation AsyncSpecialPacket + +- (id)initWithTLSSettings:(NSDictionary *)settings +{ + if((self = [super init])) + { + tlsSettings = [settings copy]; + } + return self; +} + + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation AsyncSocket + +- (id)init +{ + return [self initWithDelegate:nil userData:0]; +} + +- (id)initWithDelegate:(id)delegate +{ + return [self initWithDelegate:delegate userData:0]; +} + +// Designated initializer. +- (id)initWithDelegate:(id)delegate userData:(long)userData +{ + if((self = [super init])) + { + theFlags = DEFAULT_PREBUFFERING ? kEnablePreBuffering : 0; + theDelegate = delegate; + theUserData = userData; + + theNativeSocket4 = 0; + theNativeSocket6 = 0; + + theSocket4 = NULL; + theSource4 = NULL; + + theSocket6 = NULL; + theSource6 = NULL; + + theRunLoop = NULL; + theReadStream = NULL; + theWriteStream = NULL; + + theConnectTimer = nil; + + theReadQueue = [[NSMutableArray alloc] initWithCapacity:READQUEUE_CAPACITY]; + theCurrentRead = nil; + theReadTimer = nil; + + partialReadBuffer = [[NSMutableData alloc] initWithCapacity:READALL_CHUNKSIZE]; + + theWriteQueue = [[NSMutableArray alloc] initWithCapacity:WRITEQUEUE_CAPACITY]; + theCurrentWrite = nil; + theWriteTimer = nil; + + // Socket context + NSAssert(sizeof(CFSocketContext) == sizeof(CFStreamClientContext), @"CFSocketContext != CFStreamClientContext"); + theContext.version = 0; + theContext.info = (__bridge void *)(self); + theContext.retain = nil; + theContext.release = nil; + theContext.copyDescription = nil; + + // Default run loop modes + theRunLoopModes = [NSArray arrayWithObject:NSDefaultRunLoopMode]; + } + return self; +} + +// The socket may been initialized in a connected state and auto-released, so this should close it down cleanly. +- (void)dealloc +{ + [self close]; + [NSObject cancelPreviousPerformRequestsWithTarget:self]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Thread-Safety +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)checkForThreadSafety +{ + if (theRunLoop && (theRunLoop != CFRunLoopGetCurrent())) + { + // AsyncSocket is RunLoop based. + // It is designed to be run and accessed from a particular thread/runloop. + // As such, it is faster as it does not have the overhead of locks/synchronization. + // + // However, this places a minimal requirement on the developer to maintain thread-safety. + // If you are seeing errors or crashes in AsyncSocket, + // it is very likely that thread-safety has been broken. + // This method may be enabled via the DEBUG_THREAD_SAFETY macro, + // and will allow you to discover the place in your code where thread-safety is being broken. + // + // Note: + // + // If you find you constantly need to access your socket from various threads, + // you may prefer to use GCDAsyncSocket which is thread-safe. + + [NSException raise:AsyncSocketException + format:@"Attempting to access AsyncSocket instance from incorrect thread."]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Accessors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (long)userData +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + return theUserData; +} + +- (void)setUserData:(long)userData +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + theUserData = userData; +} + +- (id)delegate +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + return theDelegate; +} + +- (void)setDelegate:(id)delegate +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + theDelegate = delegate; +} + +- (BOOL)canSafelySetDelegate +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + return ([theReadQueue count] == 0 && [theWriteQueue count] == 0 && theCurrentRead == nil && theCurrentWrite == nil); +} + +- (CFSocketRef)getCFSocket +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + if(theSocket4) + return theSocket4; + else + return theSocket6; +} + +- (CFReadStreamRef)getCFReadStream +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + return theReadStream; +} + +- (CFWriteStreamRef)getCFWriteStream +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + return theWriteStream; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Progress +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (float)progressOfReadReturningTag:(long *)tag bytesDone:(NSUInteger *)done total:(NSUInteger *)total +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + // Check to make sure we're actually reading something right now, + // and that the read packet isn't an AsyncSpecialPacket (upgrade to TLS). + if (!theCurrentRead || ![theCurrentRead isKindOfClass:[AsyncReadPacket class]]) + { + if (tag != NULL) *tag = 0; + if (done != NULL) *done = 0; + if (total != NULL) *total = 0; + + return NAN; + } + + // It's only possible to know the progress of our read if we're reading to a certain length. + // If we're reading to data, we of course have no idea when the data will arrive. + // If we're reading to timeout, then we have no idea when the next chunk of data will arrive. + + NSUInteger d = theCurrentRead->bytesDone; + NSUInteger t = theCurrentRead->readLength; + + if (tag != NULL) *tag = theCurrentRead->tag; + if (done != NULL) *done = d; + if (total != NULL) *total = t; + + if (t > 0.0) + return (float)d / (float)t; + else + return 1.0F; +} + +- (float)progressOfWriteReturningTag:(long *)tag bytesDone:(NSUInteger *)done total:(NSUInteger *)total +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + // Check to make sure we're actually writing something right now, + // and that the write packet isn't an AsyncSpecialPacket (upgrade to TLS). + if (!theCurrentWrite || ![theCurrentWrite isKindOfClass:[AsyncWritePacket class]]) + { + if (tag != NULL) *tag = 0; + if (done != NULL) *done = 0; + if (total != NULL) *total = 0; + + return NAN; + } + + NSUInteger d = theCurrentWrite->bytesDone; + NSUInteger t = [theCurrentWrite->buffer length]; + + if (tag != NULL) *tag = theCurrentWrite->tag; + if (done != NULL) *done = d; + if (total != NULL) *total = t; + + return (float)d / (float)t; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Run Loop +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)runLoopAddSource:(CFRunLoopSourceRef)source +{ + for (NSString *runLoopMode in theRunLoopModes) + { + CFRunLoopAddSource(theRunLoop, source, (__bridge CFStringRef)runLoopMode); + } +} + +- (void)runLoopRemoveSource:(CFRunLoopSourceRef)source +{ + for (NSString *runLoopMode in theRunLoopModes) + { + CFRunLoopRemoveSource(theRunLoop, source, (__bridge CFStringRef)runLoopMode); + } +} + +- (void)runLoopAddSource:(CFRunLoopSourceRef)source mode:(NSString *)runLoopMode +{ + CFRunLoopAddSource(theRunLoop, source, (__bridge CFStringRef)runLoopMode); +} + +- (void)runLoopRemoveSource:(CFRunLoopSourceRef)source mode:(NSString *)runLoopMode +{ + CFRunLoopRemoveSource(theRunLoop, source, (__bridge CFStringRef)runLoopMode); +} + +- (void)runLoopAddTimer:(NSTimer *)timer +{ + for (NSString *runLoopMode in theRunLoopModes) + { + CFRunLoopAddTimer(theRunLoop, (__bridge CFRunLoopTimerRef)timer, (__bridge CFStringRef)runLoopMode); + } +} + +- (void)runLoopRemoveTimer:(NSTimer *)timer +{ + for (NSString *runLoopMode in theRunLoopModes) + { + CFRunLoopRemoveTimer(theRunLoop, (__bridge CFRunLoopTimerRef)timer, (__bridge CFStringRef)runLoopMode); + } +} + +- (void)runLoopAddTimer:(NSTimer *)timer mode:(NSString *)runLoopMode +{ + CFRunLoopAddTimer(theRunLoop, (__bridge CFRunLoopTimerRef)timer, (__bridge CFStringRef)runLoopMode); +} + +- (void)runLoopRemoveTimer:(NSTimer *)timer mode:(NSString *)runLoopMode +{ + CFRunLoopRemoveTimer(theRunLoop, (__bridge CFRunLoopTimerRef)timer, (__bridge CFStringRef)runLoopMode); +} + +- (void)runLoopUnscheduleReadStream +{ + for (NSString *runLoopMode in theRunLoopModes) + { + CFReadStreamUnscheduleFromRunLoop(theReadStream, theRunLoop, (__bridge CFStringRef)runLoopMode); + } + CFReadStreamSetClient(theReadStream, kCFStreamEventNone, NULL, NULL); +} + +- (void)runLoopUnscheduleWriteStream +{ + for (NSString *runLoopMode in theRunLoopModes) + { + CFWriteStreamUnscheduleFromRunLoop(theWriteStream, theRunLoop, (__bridge CFStringRef)runLoopMode); + } + CFWriteStreamSetClient(theWriteStream, kCFStreamEventNone, NULL, NULL); +} + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * See the header file for a full explanation of pre-buffering. +**/ +- (void)enablePreBuffering +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + theFlags |= kEnablePreBuffering; +} + +/** + * See the header file for a full explanation of this method. +**/ +- (BOOL)moveToRunLoop:(NSRunLoop *)runLoop +{ + NSAssert((theRunLoop == NULL) || (theRunLoop == CFRunLoopGetCurrent()), + @"moveToRunLoop must be called from within the current RunLoop!"); + + if(runLoop == nil) + { + return NO; + } + if(theRunLoop == [runLoop getCFRunLoop]) + { + return YES; + } + + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + theFlags &= ~kDequeueReadScheduled; + theFlags &= ~kDequeueWriteScheduled; + + if(theReadStream && theWriteStream) + { + [self runLoopUnscheduleReadStream]; + [self runLoopUnscheduleWriteStream]; + } + + if(theSource4) [self runLoopRemoveSource:theSource4]; + if(theSource6) [self runLoopRemoveSource:theSource6]; + + if(theReadTimer) [self runLoopRemoveTimer:theReadTimer]; + if(theWriteTimer) [self runLoopRemoveTimer:theWriteTimer]; + + theRunLoop = [runLoop getCFRunLoop]; + + if(theReadTimer) [self runLoopAddTimer:theReadTimer]; + if(theWriteTimer) [self runLoopAddTimer:theWriteTimer]; + + if(theSource4) [self runLoopAddSource:theSource4]; + if(theSource6) [self runLoopAddSource:theSource6]; + + if(theReadStream && theWriteStream) + { + if(![self attachStreamsToRunLoop:runLoop error:nil]) + { + return NO; + } + } + + [runLoop performSelector:@selector(maybeDequeueRead) target:self argument:nil order:0 modes:theRunLoopModes]; + [runLoop performSelector:@selector(maybeDequeueWrite) target:self argument:nil order:0 modes:theRunLoopModes]; + [runLoop performSelector:@selector(maybeScheduleDisconnect) target:self argument:nil order:0 modes:theRunLoopModes]; + + return YES; +} + +/** + * See the header file for a full explanation of this method. +**/ +- (BOOL)setRunLoopModes:(NSArray *)runLoopModes +{ + NSAssert((theRunLoop == NULL) || (theRunLoop == CFRunLoopGetCurrent()), + @"setRunLoopModes must be called from within the current RunLoop!"); + + if([runLoopModes count] == 0) + { + return NO; + } + if([theRunLoopModes isEqualToArray:runLoopModes]) + { + return YES; + } + + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + theFlags &= ~kDequeueReadScheduled; + theFlags &= ~kDequeueWriteScheduled; + + if(theReadStream && theWriteStream) + { + [self runLoopUnscheduleReadStream]; + [self runLoopUnscheduleWriteStream]; + } + + if(theSource4) [self runLoopRemoveSource:theSource4]; + if(theSource6) [self runLoopRemoveSource:theSource6]; + + if(theReadTimer) [self runLoopRemoveTimer:theReadTimer]; + if(theWriteTimer) [self runLoopRemoveTimer:theWriteTimer]; + + theRunLoopModes = [runLoopModes copy]; + + if(theReadTimer) [self runLoopAddTimer:theReadTimer]; + if(theWriteTimer) [self runLoopAddTimer:theWriteTimer]; + + if(theSource4) [self runLoopAddSource:theSource4]; + if(theSource6) [self runLoopAddSource:theSource6]; + + if(theReadStream && theWriteStream) + { + // Note: theRunLoop variable is a CFRunLoop, and NSRunLoop is NOT toll-free bridged with CFRunLoop. + // So we cannot pass theRunLoop to the method below, which is expecting a NSRunLoop parameter. + // Instead we pass nil, which will result in the method properly using the current run loop. + + if(![self attachStreamsToRunLoop:nil error:nil]) + { + return NO; + } + } + + [self performSelector:@selector(maybeDequeueRead) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + [self performSelector:@selector(maybeDequeueWrite) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + [self performSelector:@selector(maybeScheduleDisconnect) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + + return YES; +} + +- (BOOL)addRunLoopMode:(NSString *)runLoopMode +{ + NSAssert((theRunLoop == NULL) || (theRunLoop == CFRunLoopGetCurrent()), + @"addRunLoopMode must be called from within the current RunLoop!"); + + if(runLoopMode == nil) + { + return NO; + } + if([theRunLoopModes containsObject:runLoopMode]) + { + return YES; + } + + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + theFlags &= ~kDequeueReadScheduled; + theFlags &= ~kDequeueWriteScheduled; + + NSArray *newRunLoopModes = [theRunLoopModes arrayByAddingObject:runLoopMode]; + theRunLoopModes = newRunLoopModes; + + if(theReadTimer) [self runLoopAddTimer:theReadTimer mode:runLoopMode]; + if(theWriteTimer) [self runLoopAddTimer:theWriteTimer mode:runLoopMode]; + + if(theSource4) [self runLoopAddSource:theSource4 mode:runLoopMode]; + if(theSource6) [self runLoopAddSource:theSource6 mode:runLoopMode]; + + if(theReadStream && theWriteStream) + { + CFReadStreamScheduleWithRunLoop(theReadStream, CFRunLoopGetCurrent(), (__bridge CFStringRef)runLoopMode); + CFWriteStreamScheduleWithRunLoop(theWriteStream, CFRunLoopGetCurrent(), (__bridge CFStringRef)runLoopMode); + } + + [self performSelector:@selector(maybeDequeueRead) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + [self performSelector:@selector(maybeDequeueWrite) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + [self performSelector:@selector(maybeScheduleDisconnect) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + + return YES; +} + +- (BOOL)removeRunLoopMode:(NSString *)runLoopMode +{ + NSAssert((theRunLoop == NULL) || (theRunLoop == CFRunLoopGetCurrent()), + @"addRunLoopMode must be called from within the current RunLoop!"); + + if(runLoopMode == nil) + { + return NO; + } + if(![theRunLoopModes containsObject:runLoopMode]) + { + return YES; + } + + NSMutableArray *newRunLoopModes = [theRunLoopModes mutableCopy]; + [newRunLoopModes removeObject:runLoopMode]; + + if([newRunLoopModes count] == 0) + { + return NO; + } + + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + theFlags &= ~kDequeueReadScheduled; + theFlags &= ~kDequeueWriteScheduled; + + theRunLoopModes = [newRunLoopModes copy]; + + if(theReadTimer) [self runLoopRemoveTimer:theReadTimer mode:runLoopMode]; + if(theWriteTimer) [self runLoopRemoveTimer:theWriteTimer mode:runLoopMode]; + + if(theSource4) [self runLoopRemoveSource:theSource4 mode:runLoopMode]; + if(theSource6) [self runLoopRemoveSource:theSource6 mode:runLoopMode]; + + if(theReadStream && theWriteStream) + { + CFReadStreamScheduleWithRunLoop(theReadStream, CFRunLoopGetCurrent(), (__bridge CFStringRef)runLoopMode); + CFWriteStreamScheduleWithRunLoop(theWriteStream, CFRunLoopGetCurrent(), (__bridge CFStringRef)runLoopMode); + } + + [self performSelector:@selector(maybeDequeueRead) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + [self performSelector:@selector(maybeDequeueWrite) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + [self performSelector:@selector(maybeScheduleDisconnect) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + + return YES; +} + +- (NSArray *)runLoopModes +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + return theRunLoopModes; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Accepting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)acceptOnPort:(UInt16)port error:(NSError **)errPtr +{ + return [self acceptOnInterface:nil port:port error:errPtr]; +} + +/** + * To accept on a certain interface, pass the address to accept on. + * To accept on any interface, pass nil or an empty string. + * To accept only connections from localhost pass "localhost" or "loopback". +**/ +- (BOOL)acceptOnInterface:(NSString *)interface port:(UInt16)port error:(NSError **)errPtr +{ + if (theDelegate == NULL) + { + [NSException raise:AsyncSocketException + format:@"Attempting to accept without a delegate. Set a delegate first."]; + } + + if (![self isDisconnected]) + { + [NSException raise:AsyncSocketException + format:@"Attempting to accept while connected or accepting connections. Disconnect first."]; + } + + // Clear queues (spurious read/write requests post disconnect) + [self emptyQueues]; + + // Set up the listen sockaddr structs if needed. + + NSData *address4 = nil, *address6 = nil; + if(interface == nil || ([interface length] == 0)) + { + // Accept on ANY address + struct sockaddr_in nativeAddr4; + nativeAddr4.sin_len = sizeof(struct sockaddr_in); + nativeAddr4.sin_family = AF_INET; + nativeAddr4.sin_port = htons(port); + nativeAddr4.sin_addr.s_addr = htonl(INADDR_ANY); + memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_any; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures for CFSocketSetAddress. + address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else if([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) + { + // Accept only on LOOPBACK address + struct sockaddr_in nativeAddr4; + nativeAddr4.sin_len = sizeof(struct sockaddr_in); + nativeAddr4.sin_family = AF_INET; + nativeAddr4.sin_port = htons(port); + nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_loopback; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures for CFSocketSetAddress. + address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; + address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_PASSIVE; + + int error = getaddrinfo([interface UTF8String], [portStr UTF8String], &hints, &res0); + + if (error) + { + if (errPtr) + { + NSString *errMsg = [NSString stringWithCString:gai_strerror(error) encoding:NSASCIIStringEncoding]; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + *errPtr = [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:error userInfo:info]; + } + } + else + { + for (res = res0; res; res = res->ai_next) + { + if (!address4 && (res->ai_family == AF_INET)) + { + // Found IPv4 address + // Wrap the native address structures for CFSocketSetAddress. + address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + else if (!address6 && (res->ai_family == AF_INET6)) + { + // Found IPv6 address + // Wrap the native address structures for CFSocketSetAddress. + address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + } + freeaddrinfo(res0); + } + + if(!address4 && !address6) return NO; + } + + // Create the sockets. + + if (address4) + { + theSocket4 = [self newAcceptSocketForAddress:address4 error:errPtr]; + if (theSocket4 == NULL) goto Failed; + } + + if (address6) + { + theSocket6 = [self newAcceptSocketForAddress:address6 error:errPtr]; + + // Note: The iPhone doesn't currently support IPv6 + +#if !TARGET_OS_IPHONE + if (theSocket6 == NULL) goto Failed; +#endif + } + + // Attach the sockets to the run loop so that callback methods work + + [self attachSocketsToRunLoop:nil error:nil]; + + // Set the SO_REUSEADDR flags. + + int reuseOn = 1; + if (theSocket4) setsockopt(CFSocketGetNative(theSocket4), SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + if (theSocket6) setsockopt(CFSocketGetNative(theSocket6), SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + + // Set the local bindings which causes the sockets to start listening. + + CFSocketError err; + if (theSocket4) + { + err = CFSocketSetAddress(theSocket4, (__bridge CFDataRef)address4); + if (err != kCFSocketSuccess) goto Failed; + + //NSLog(@"theSocket4: %hu", [self localPortFromCFSocket4:theSocket4]); + } + + if(port == 0 && theSocket4 && theSocket6) + { + // The user has passed in port 0, which means he wants to allow the kernel to choose the port for them + // However, the kernel will choose a different port for both theSocket4 and theSocket6 + // So we grab the port the kernel choose for theSocket4, and set it as the port for theSocket6 + UInt16 chosenPort = [self localPortFromCFSocket4:theSocket4]; + + struct sockaddr_in6 *pSockAddr6 = (struct sockaddr_in6 *)[address6 bytes]; + if (pSockAddr6) // If statement to quiet the static analyzer + { + pSockAddr6->sin6_port = htons(chosenPort); + } + } + + if (theSocket6) + { + err = CFSocketSetAddress(theSocket6, (__bridge CFDataRef)address6); + if (err != kCFSocketSuccess) goto Failed; + + //NSLog(@"theSocket6: %hu", [self localPortFromCFSocket6:theSocket6]); + } + + theFlags |= kDidStartDelegate; + return YES; + +Failed: + if(errPtr) *errPtr = [self getSocketError]; + if(theSocket4 != NULL) + { + CFSocketInvalidate(theSocket4); + CFRelease(theSocket4); + theSocket4 = NULL; + } + if(theSocket6 != NULL) + { + CFSocketInvalidate(theSocket6); + CFRelease(theSocket6); + theSocket6 = NULL; + } + return NO; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Connecting +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)connectToHost:(NSString*)hostname onPort:(UInt16)port error:(NSError **)errPtr +{ + return [self connectToHost:hostname onPort:port withTimeout:-1 error:errPtr]; +} + +/** + * This method creates an initial CFReadStream and CFWriteStream to the given host on the given port. + * The connection is then opened, and the corresponding CFSocket will be extracted after the connection succeeds. + * + * Thus the delegate will have access to the CFReadStream and CFWriteStream prior to connection, + * specifically in the onSocketWillConnect: method. +**/ +- (BOOL)connectToHost:(NSString *)hostname + onPort:(UInt16)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + if (theDelegate == NULL) + { + [NSException raise:AsyncSocketException + format:@"Attempting to connect without a delegate. Set a delegate first."]; + } + + if (![self isDisconnected]) + { + [NSException raise:AsyncSocketException + format:@"Attempting to connect while connected or accepting connections. Disconnect first."]; + } + + // Clear queues (spurious read/write requests post disconnect) + [self emptyQueues]; + + if(![self createStreamsToHost:hostname onPort:port error:errPtr]) goto Failed; + if(![self attachStreamsToRunLoop:nil error:errPtr]) goto Failed; + if(![self configureStreamsAndReturnError:errPtr]) goto Failed; + if(![self openStreamsAndReturnError:errPtr]) goto Failed; + + [self startConnectTimeout:timeout]; + theFlags |= kDidStartDelegate; + + return YES; + +Failed: + [self close]; + return NO; +} + +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterfaceAddress:nil withTimeout:-1 error:errPtr]; +} + +/** + * This method creates an initial CFSocket to the given address. + * The connection is then opened, and the corresponding CFReadStream and CFWriteStream will be + * created from the low-level sockets after the connection succeeds. + * + * Thus the delegate will have access to the CFSocket and CFSocketNativeHandle (BSD socket) prior to connection, + * specifically in the onSocketWillConnect: method. + * + * Note: The NSData parameter is expected to be a sockaddr structure. For example, an NSData object returned from + * NSNetService addresses method. + * If you have an existing struct sockaddr you can convert it to an NSData object like so: + * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; + * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr +{ + return [self connectToAddress:remoteAddr viaInterfaceAddress:nil withTimeout:timeout error:errPtr]; +} + +/** + * This method is similar to the one above, but allows you to specify which socket interface + * the connection should run over. E.g. ethernet, wifi, bluetooth, etc. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr + viaInterfaceAddress:(NSData *)interfaceAddr + withTimeout:(NSTimeInterval)timeout + error:(NSError **)errPtr +{ + if (theDelegate == NULL) + { + [NSException raise:AsyncSocketException + format:@"Attempting to connect without a delegate. Set a delegate first."]; + } + + if (![self isDisconnected]) + { + [NSException raise:AsyncSocketException + format:@"Attempting to connect while connected or accepting connections. Disconnect first."]; + } + + // Clear queues (spurious read/write requests post disconnect) + [self emptyQueues]; + + if(![self createSocketForAddress:remoteAddr error:errPtr]) goto Failed; + if(![self bindSocketToAddress:interfaceAddr error:errPtr]) goto Failed; + if(![self attachSocketsToRunLoop:nil error:errPtr]) goto Failed; + if(![self configureSocketAndReturnError:errPtr]) goto Failed; + if(![self connectSocketToAddress:remoteAddr error:errPtr]) goto Failed; + + [self startConnectTimeout:timeout]; + theFlags |= kDidStartDelegate; + + return YES; + +Failed: + [self close]; + return NO; +} + +- (void)startConnectTimeout:(NSTimeInterval)timeout +{ + if(timeout >= 0.0) + { + theConnectTimer = [NSTimer timerWithTimeInterval:timeout + target:self + selector:@selector(doConnectTimeout:) + userInfo:nil + repeats:NO]; + [self runLoopAddTimer:theConnectTimer]; + } +} + +- (void)endConnectTimeout +{ + [theConnectTimer invalidate]; + theConnectTimer = nil; +} + +- (void)doConnectTimeout:(NSTimer *)timer +{ + #pragma unused(timer) + + [self endConnectTimeout]; + [self closeWithError:[self getConnectTimeoutError]]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Socket Implementation +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Creates the accept sockets. + * Returns true if either IPv4 or IPv6 is created. + * If either is missing, an error is returned (even though the method may return true). +**/ +- (CFSocketRef)newAcceptSocketForAddress:(NSData *)addr error:(NSError **)errPtr +{ + struct sockaddr *pSockAddr = (struct sockaddr *)[addr bytes]; + int addressFamily = pSockAddr->sa_family; + + CFSocketRef theSocket = CFSocketCreate(kCFAllocatorDefault, + addressFamily, + SOCK_STREAM, + 0, + kCFSocketAcceptCallBack, // Callback flags + (CFSocketCallBack)&MyCFSocketCallback, // Callback method + &theContext); + + if(theSocket == NULL) + { + if(errPtr) *errPtr = [self getSocketError]; + } + + return theSocket; +} + +- (BOOL)createSocketForAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + struct sockaddr *pSockAddr = (struct sockaddr *)[remoteAddr bytes]; + + if(pSockAddr->sa_family == AF_INET) + { + theSocket4 = CFSocketCreate(NULL, // Default allocator + PF_INET, // Protocol Family + SOCK_STREAM, // Socket Type + IPPROTO_TCP, // Protocol + kCFSocketConnectCallBack, // Callback flags + (CFSocketCallBack)&MyCFSocketCallback, // Callback method + &theContext); // Socket Context + + if(theSocket4 == NULL) + { + if (errPtr) *errPtr = [self getSocketError]; + return NO; + } + } + else if(pSockAddr->sa_family == AF_INET6) + { + theSocket6 = CFSocketCreate(NULL, // Default allocator + PF_INET6, // Protocol Family + SOCK_STREAM, // Socket Type + IPPROTO_TCP, // Protocol + kCFSocketConnectCallBack, // Callback flags + (CFSocketCallBack)&MyCFSocketCallback, // Callback method + &theContext); // Socket Context + + if(theSocket6 == NULL) + { + if (errPtr) *errPtr = [self getSocketError]; + return NO; + } + } + else + { + if (errPtr) + { + NSString *errMsg = @"Remote address is not IPv4 or IPv6"; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + *errPtr = [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketCFSocketError userInfo:info]; + } + return NO; + } + + return YES; +} + +- (BOOL)bindSocketToAddress:(NSData *)interfaceAddr error:(NSError **)errPtr +{ + if (interfaceAddr == nil) return YES; + + struct sockaddr *pSockAddr = (struct sockaddr *)[interfaceAddr bytes]; + + CFSocketRef theSocket = (theSocket4 != NULL) ? theSocket4 : theSocket6; + NSAssert((theSocket != NULL), @"bindSocketToAddress called without valid socket"); + + CFSocketNativeHandle nativeSocket = CFSocketGetNative(theSocket); + + if (pSockAddr->sa_family == AF_INET || pSockAddr->sa_family == AF_INET6) + { + int result = bind(nativeSocket, pSockAddr, (socklen_t)[interfaceAddr length]); + if (result != 0) + { + if (errPtr) *errPtr = [self getErrnoError]; + return NO; + } + } + else + { + if (errPtr) + { + NSString *errMsg = @"Interface address is not IPv4 or IPv6"; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + *errPtr = [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketCFSocketError userInfo:info]; + } + return NO; + } + + return YES; +} + +/** + * Adds the CFSocket's to the run-loop so that callbacks will work properly. +**/ +- (BOOL)attachSocketsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr +{ + #pragma unused(errPtr) + + // Get the CFRunLoop to which the socket should be attached. + theRunLoop = (runLoop == nil) ? CFRunLoopGetCurrent() : [runLoop getCFRunLoop]; + + if(theSocket4) + { + theSource4 = CFSocketCreateRunLoopSource (kCFAllocatorDefault, theSocket4, 0); + [self runLoopAddSource:theSource4]; + } + + if(theSocket6) + { + theSource6 = CFSocketCreateRunLoopSource (kCFAllocatorDefault, theSocket6, 0); + [self runLoopAddSource:theSource6]; + } + + return YES; +} + +/** + * Allows the delegate method to configure the CFSocket or CFNativeSocket as desired before we connect. + * Note that the CFReadStream and CFWriteStream will not be available until after the connection is opened. +**/ +- (BOOL)configureSocketAndReturnError:(NSError **)errPtr +{ + // Call the delegate method for further configuration. + if([theDelegate respondsToSelector:@selector(onSocketWillConnect:)]) + { + if([theDelegate onSocketWillConnect:self] == NO) + { + if (errPtr) *errPtr = [self getAbortError]; + return NO; + } + } + return YES; +} + +- (BOOL)connectSocketToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + // Start connecting to the given address in the background + // The MyCFSocketCallback method will be called when the connection succeeds or fails + if(theSocket4) + { + CFSocketError err = CFSocketConnectToAddress(theSocket4, (__bridge CFDataRef)remoteAddr, -1); + if(err != kCFSocketSuccess) + { + if (errPtr) *errPtr = [self getSocketError]; + return NO; + } + } + else if(theSocket6) + { + CFSocketError err = CFSocketConnectToAddress(theSocket6, (__bridge CFDataRef)remoteAddr, -1); + if(err != kCFSocketSuccess) + { + if (errPtr) *errPtr = [self getSocketError]; + return NO; + } + } + + return YES; +} + +/** + * Attempt to make the new socket. + * If an error occurs, ignore this event. +**/ +- (void)doAcceptFromSocket:(CFSocketRef)parentSocket withNewNativeSocket:(CFSocketNativeHandle)newNativeSocket +{ + if(newNativeSocket) + { + // New socket inherits same delegate and run loop modes. + // Note: We use [self class] to support subclassing AsyncSocket. + AsyncSocket *newSocket = [[[self class] alloc] initWithDelegate:theDelegate]; + [newSocket setRunLoopModes:theRunLoopModes]; + + if (![newSocket createStreamsFromNative:newNativeSocket error:nil]) + { + [newSocket close]; + return; + } + + if (parentSocket == theSocket4) + newSocket->theNativeSocket4 = newNativeSocket; + else + newSocket->theNativeSocket6 = newNativeSocket; + + if ([theDelegate respondsToSelector:@selector(onSocket:didAcceptNewSocket:)]) + [theDelegate onSocket:self didAcceptNewSocket:newSocket]; + + newSocket->theFlags |= kDidStartDelegate; + + NSRunLoop *runLoop = nil; + if ([theDelegate respondsToSelector:@selector(onSocket:wantsRunLoopForNewSocket:)]) + { + runLoop = [theDelegate onSocket:self wantsRunLoopForNewSocket:newSocket]; + } + + if(![newSocket attachStreamsToRunLoop:runLoop error:nil]) goto Failed; + if(![newSocket configureStreamsAndReturnError:nil]) goto Failed; + if(![newSocket openStreamsAndReturnError:nil]) goto Failed; + + return; + + Failed: + [newSocket close]; + } +} + +/** + * This method is called as a result of connectToAddress:withTimeout:error:. + * At this point we have an open CFSocket from which we need to create our read and write stream. +**/ +- (void)doSocketOpen:(CFSocketRef)sock withCFSocketError:(CFSocketError)socketError +{ + NSParameterAssert ((sock == theSocket4) || (sock == theSocket6)); + + if(socketError == kCFSocketTimeout || socketError == kCFSocketError) + { + [self closeWithError:[self getSocketError]]; + return; + } + + // Get the underlying native (BSD) socket + CFSocketNativeHandle nativeSocket = CFSocketGetNative(sock); + + // Store a reference to it + if (sock == theSocket4) + theNativeSocket4 = nativeSocket; + else + theNativeSocket6 = nativeSocket; + + // Setup the CFSocket so that invalidating it will not close the underlying native socket + CFSocketSetSocketFlags(sock, 0); + + // Invalidate and release the CFSocket - All we need from here on out is the nativeSocket. + // Note: If we don't invalidate the CFSocket (leaving the native socket open) + // then theReadStream and theWriteStream won't function properly. + // Specifically, their callbacks won't work, with the exception of kCFStreamEventOpenCompleted. + // + // This is likely due to the mixture of the CFSocketCreateWithNative method, + // along with the CFStreamCreatePairWithSocket method. + // The documentation for CFSocketCreateWithNative states: + // + // If a CFSocket object already exists for sock, + // the function returns the pre-existing object instead of creating a new object; + // the context, callout, and callBackTypes parameters are ignored in this case. + // + // So the CFStreamCreateWithNative method invokes the CFSocketCreateWithNative method, + // thinking that is creating a new underlying CFSocket for it's own purposes. + // When it does this, it uses the context/callout/callbackTypes parameters to setup everything appropriately. + // However, if a CFSocket already exists for the native socket, + // then it is returned (as per the documentation), which in turn screws up the CFStreams. + + CFSocketInvalidate(sock); + CFRelease(sock); + theSocket4 = NULL; + theSocket6 = NULL; + + NSError *err; + BOOL pass = YES; + + if(pass && ![self createStreamsFromNative:nativeSocket error:&err]) pass = NO; + if(pass && ![self attachStreamsToRunLoop:nil error:&err]) pass = NO; + if(pass && ![self openStreamsAndReturnError:&err]) pass = NO; + + if(!pass) + { + [self closeWithError:err]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Stream Implementation +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Creates the CFReadStream and CFWriteStream from the given native socket. + * The CFSocket may be extracted from either stream after the streams have been opened. + * + * Note: The given native socket must already be connected! +**/ +- (BOOL)createStreamsFromNative:(CFSocketNativeHandle)native error:(NSError **)errPtr +{ + // Create the socket & streams. + CFStreamCreatePairWithSocket(kCFAllocatorDefault, native, &theReadStream, &theWriteStream); + if (theReadStream == NULL || theWriteStream == NULL) + { + NSError *err = [self getStreamError]; + + NSLog(@"AsyncSocket %p couldn't create streams from accepted socket: %@", self, err); + + if (errPtr) *errPtr = err; + return NO; + } + + // Ensure the CF & BSD socket is closed when the streams are closed. + CFReadStreamSetProperty(theReadStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); + CFWriteStreamSetProperty(theWriteStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); + + return YES; +} + +/** + * Creates the CFReadStream and CFWriteStream from the given hostname and port number. + * The CFSocket may be extracted from either stream after the streams have been opened. +**/ +- (BOOL)createStreamsToHost:(NSString *)hostname onPort:(UInt16)port error:(NSError **)errPtr +{ + // Create the socket & streams. + CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)hostname, port, &theReadStream, &theWriteStream); + if (theReadStream == NULL || theWriteStream == NULL) + { + if (errPtr) *errPtr = [self getStreamError]; + return NO; + } + + // Ensure the CF & BSD socket is closed when the streams are closed. + CFReadStreamSetProperty(theReadStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); + CFWriteStreamSetProperty(theWriteStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); + + return YES; +} + +- (BOOL)attachStreamsToRunLoop:(NSRunLoop *)runLoop error:(NSError **)errPtr +{ + // Get the CFRunLoop to which the socket should be attached. + theRunLoop = (runLoop == nil) ? CFRunLoopGetCurrent() : [runLoop getCFRunLoop]; + + // Setup read stream callbacks + + CFOptionFlags readStreamEvents = kCFStreamEventHasBytesAvailable | + kCFStreamEventErrorOccurred | + kCFStreamEventEndEncountered | + kCFStreamEventOpenCompleted; + + if (!CFReadStreamSetClient(theReadStream, + readStreamEvents, + (CFReadStreamClientCallBack)&MyCFReadStreamCallback, + (CFStreamClientContext *)(&theContext))) + { + NSError *err = [self getStreamError]; + + NSLog (@"AsyncSocket %p couldn't attach read stream to run-loop,", self); + NSLog (@"Error: %@", err); + + if (errPtr) *errPtr = err; + return NO; + } + + // Setup write stream callbacks + + CFOptionFlags writeStreamEvents = kCFStreamEventCanAcceptBytes | + kCFStreamEventErrorOccurred | + kCFStreamEventEndEncountered | + kCFStreamEventOpenCompleted; + + if (!CFWriteStreamSetClient (theWriteStream, + writeStreamEvents, + (CFWriteStreamClientCallBack)&MyCFWriteStreamCallback, + (CFStreamClientContext *)(&theContext))) + { + NSError *err = [self getStreamError]; + + NSLog (@"AsyncSocket %p couldn't attach write stream to run-loop,", self); + NSLog (@"Error: %@", err); + + if (errPtr) *errPtr = err; + return NO; + } + + // Add read and write streams to run loop + + for (NSString *runLoopMode in theRunLoopModes) + { + CFReadStreamScheduleWithRunLoop(theReadStream, theRunLoop, (__bridge CFStringRef)runLoopMode); + CFWriteStreamScheduleWithRunLoop(theWriteStream, theRunLoop, (__bridge CFStringRef)runLoopMode); + } + + return YES; +} + +/** + * Allows the delegate method to configure the CFReadStream and/or CFWriteStream as desired before we connect. + * + * If being called from a connect method, + * the CFSocket and CFNativeSocket will not be available until after the connection is opened. +**/ +- (BOOL)configureStreamsAndReturnError:(NSError **)errPtr +{ + // Call the delegate method for further configuration. + if([theDelegate respondsToSelector:@selector(onSocketWillConnect:)]) + { + if([theDelegate onSocketWillConnect:self] == NO) + { + if (errPtr) *errPtr = [self getAbortError]; + return NO; + } + } + return YES; +} + +- (BOOL)openStreamsAndReturnError:(NSError **)errPtr +{ + BOOL pass = YES; + + if(pass && !CFReadStreamOpen(theReadStream)) + { + NSLog (@"AsyncSocket %p couldn't open read stream,", self); + pass = NO; + } + + if(pass && !CFWriteStreamOpen(theWriteStream)) + { + NSLog (@"AsyncSocket %p couldn't open write stream,", self); + pass = NO; + } + + if(!pass) + { + if (errPtr) *errPtr = [self getStreamError]; + } + + return pass; +} + +/** + * Called when read or write streams open. + * When the socket is connected and both streams are open, consider the AsyncSocket instance to be ready. +**/ +- (void)doStreamOpen +{ + if ((theFlags & kDidCompleteOpenForRead) && (theFlags & kDidCompleteOpenForWrite)) + { + NSError *err = nil; + + // Get the socket + if (![self setSocketFromStreamsAndReturnError: &err]) + { + NSLog (@"AsyncSocket %p couldn't get socket from streams, %@. Disconnecting.", self, err); + [self closeWithError:err]; + return; + } + + // Stop the connection attempt timeout timer + [self endConnectTimeout]; + + if ([theDelegate respondsToSelector:@selector(onSocket:didConnectToHost:port:)]) + { + [theDelegate onSocket:self didConnectToHost:[self connectedHost] port:[self connectedPort]]; + } + + // Immediately deal with any already-queued requests. + [self maybeDequeueRead]; + [self maybeDequeueWrite]; + } +} + +- (BOOL)setSocketFromStreamsAndReturnError:(NSError **)errPtr +{ + // Get the CFSocketNativeHandle from theReadStream + CFSocketNativeHandle native; + CFDataRef nativeProp = CFReadStreamCopyProperty(theReadStream, kCFStreamPropertySocketNativeHandle); + if(nativeProp == NULL) + { + if (errPtr) *errPtr = [self getStreamError]; + return NO; + } + + CFIndex nativePropLen = CFDataGetLength(nativeProp); + CFIndex nativeLen = (CFIndex)sizeof(native); + + CFIndex len = MIN(nativePropLen, nativeLen); + + CFDataGetBytes(nativeProp, CFRangeMake(0, len), (UInt8 *)&native); + CFRelease(nativeProp); + + CFSocketRef theSocket = CFSocketCreateWithNative(kCFAllocatorDefault, native, 0, NULL, NULL); + if(theSocket == NULL) + { + if (errPtr) *errPtr = [self getSocketError]; + return NO; + } + + // Determine whether the connection was IPv4 or IPv6. + // We may already know if this was an accepted socket, + // or if the connectToAddress method was used. + // In either of the above two cases, the native socket variable would already be set. + + if (theNativeSocket4 > 0) + { + theSocket4 = theSocket; + return YES; + } + if (theNativeSocket6 > 0) + { + theSocket6 = theSocket; + return YES; + } + + CFDataRef peeraddr = CFSocketCopyPeerAddress(theSocket); + if(peeraddr == NULL) + { + NSLog(@"AsyncSocket couldn't determine IP version of socket"); + + CFRelease(theSocket); + + if (errPtr) *errPtr = [self getSocketError]; + return NO; + } + struct sockaddr *sa = (struct sockaddr *)CFDataGetBytePtr(peeraddr); + + if(sa->sa_family == AF_INET) + { + theSocket4 = theSocket; + theNativeSocket4 = native; + } + else + { + theSocket6 = theSocket; + theNativeSocket6 = native; + } + + CFRelease(peeraddr); + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Disconnect Implementation +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Sends error message and disconnects +- (void)closeWithError:(NSError *)err +{ + theFlags |= kClosingWithError; + + if (theFlags & kDidStartDelegate) + { + // Try to salvage what data we can. + [self recoverUnreadData]; + + // Let the delegate know, so it can try to recover if it likes. + if ([theDelegate respondsToSelector:@selector(onSocket:willDisconnectWithError:)]) + { + [theDelegate onSocket:self willDisconnectWithError:err]; + } + } + [self close]; +} + +// Prepare partially read data for recovery. +- (void)recoverUnreadData +{ + if(theCurrentRead != nil) + { + // We never finished the current read. + // Check to see if it's a normal read packet (not AsyncSpecialPacket) and if it had read anything yet. + + if(([theCurrentRead isKindOfClass:[AsyncReadPacket class]]) && (theCurrentRead->bytesDone > 0)) + { + // We need to move its data into the front of the partial read buffer. + + void *buffer = [theCurrentRead->buffer mutableBytes] + theCurrentRead->startOffset; + + [partialReadBuffer replaceBytesInRange:NSMakeRange(0, 0) + withBytes:buffer + length:theCurrentRead->bytesDone]; + } + } + + [self emptyQueues]; +} + +- (void)emptyQueues +{ + if (theCurrentRead != nil) [self endCurrentRead]; + if (theCurrentWrite != nil) [self endCurrentWrite]; + + [theReadQueue removeAllObjects]; + [theWriteQueue removeAllObjects]; + + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(maybeDequeueRead) object:nil]; + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(maybeDequeueWrite) object:nil]; + + theFlags &= ~kDequeueReadScheduled; + theFlags &= ~kDequeueWriteScheduled; +} + +/** + * Disconnects. This is called for both error and clean disconnections. +**/ +- (void)close +{ + // Empty queues + [self emptyQueues]; + + // Clear partialReadBuffer (pre-buffer and also unreadData buffer in case of error) + [partialReadBuffer replaceBytesInRange:NSMakeRange(0, [partialReadBuffer length]) withBytes:NULL length:0]; + + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(disconnect) object:nil]; + + // Stop the connection attempt timeout timer + if (theConnectTimer != nil) + { + [self endConnectTimeout]; + } + + // Close streams. + if (theReadStream != NULL) + { + [self runLoopUnscheduleReadStream]; + CFReadStreamClose(theReadStream); + CFRelease(theReadStream); + theReadStream = NULL; + } + if (theWriteStream != NULL) + { + [self runLoopUnscheduleWriteStream]; + CFWriteStreamClose(theWriteStream); + CFRelease(theWriteStream); + theWriteStream = NULL; + } + + // Close sockets. + if (theSocket4 != NULL) + { + CFSocketInvalidate (theSocket4); + CFRelease (theSocket4); + theSocket4 = NULL; + } + if (theSocket6 != NULL) + { + CFSocketInvalidate (theSocket6); + CFRelease (theSocket6); + theSocket6 = NULL; + } + + // Closing the streams or sockets resulted in closing the underlying native socket + theNativeSocket4 = 0; + theNativeSocket6 = 0; + + // Remove run loop sources + if (theSource4 != NULL) + { + [self runLoopRemoveSource:theSource4]; + CFRelease (theSource4); + theSource4 = NULL; + } + if (theSource6 != NULL) + { + [self runLoopRemoveSource:theSource6]; + CFRelease (theSource6); + theSource6 = NULL; + } + theRunLoop = NULL; + + // If the client has passed the connect/accept method, then the connection has at least begun. + // Notify delegate that it is now ending. + BOOL shouldCallDelegate = (theFlags & kDidStartDelegate); + + // Clear all flags (except the pre-buffering flag, which should remain as is) + theFlags &= kEnablePreBuffering; + + if (shouldCallDelegate) + { + if ([theDelegate respondsToSelector: @selector(onSocketDidDisconnect:)]) + { + [theDelegate onSocketDidDisconnect:self]; + } + } + + // Do not access any instance variables after calling onSocketDidDisconnect. + // This gives the delegate freedom to release us without returning here and crashing. +} + +/** + * Disconnects immediately. Any pending reads or writes are dropped. +**/ +- (void)disconnect +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + [self close]; +} + +/** + * Diconnects after all pending reads have completed. +**/ +- (void)disconnectAfterReading +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + theFlags |= (kForbidReadsWrites | kDisconnectAfterReads); + + [self maybeScheduleDisconnect]; +} + +/** + * Disconnects after all pending writes have completed. +**/ +- (void)disconnectAfterWriting +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + theFlags |= (kForbidReadsWrites | kDisconnectAfterWrites); + + [self maybeScheduleDisconnect]; +} + +/** + * Disconnects after all pending reads and writes have completed. +**/ +- (void)disconnectAfterReadingAndWriting +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + theFlags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); + + [self maybeScheduleDisconnect]; +} + +/** + * Schedules a call to disconnect if possible. + * That is, if all writes have completed, and we're set to disconnect after writing, + * or if all reads have completed, and we're set to disconnect after reading. +**/ +- (void)maybeScheduleDisconnect +{ + BOOL shouldDisconnect = NO; + + if(theFlags & kDisconnectAfterReads) + { + if(([theReadQueue count] == 0) && (theCurrentRead == nil)) + { + if(theFlags & kDisconnectAfterWrites) + { + if(([theWriteQueue count] == 0) && (theCurrentWrite == nil)) + { + shouldDisconnect = YES; + } + } + else + { + shouldDisconnect = YES; + } + } + } + else if(theFlags & kDisconnectAfterWrites) + { + if(([theWriteQueue count] == 0) && (theCurrentWrite == nil)) + { + shouldDisconnect = YES; + } + } + + if(shouldDisconnect) + { + [self performSelector:@selector(disconnect) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + } +} + +/** + * In the event of an error, this method may be called during onSocket:willDisconnectWithError: to read + * any data that's left on the socket. +**/ +- (NSData *)unreadData +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + // Ensure this method will only return data in the event of an error + if (!(theFlags & kClosingWithError)) return nil; + + if (theReadStream == NULL) return nil; + + NSUInteger totalBytesRead = [partialReadBuffer length]; + + BOOL error = NO; + while (!error && CFReadStreamHasBytesAvailable(theReadStream)) + { + if (totalBytesRead == [partialReadBuffer length]) + { + [partialReadBuffer increaseLengthBy:READALL_CHUNKSIZE]; + } + + // Number of bytes to read is space left in packet buffer. + NSUInteger bytesToRead = [partialReadBuffer length] - totalBytesRead; + + // Read data into packet buffer + UInt8 *packetbuf = (UInt8 *)( [partialReadBuffer mutableBytes] + totalBytesRead ); + + CFIndex result = CFReadStreamRead(theReadStream, packetbuf, bytesToRead); + + // Check results + if (result < 0) + { + error = YES; + } + else + { + CFIndex bytesRead = result; + + totalBytesRead += bytesRead; + } + } + + [partialReadBuffer setLength:totalBytesRead]; + + return partialReadBuffer; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns a standard error object for the current errno value. + * Errno is used for low-level BSD socket errors. +**/ +- (NSError *)getErrnoError +{ + NSString *errorMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errorMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +/** + * Returns a standard error message for a CFSocket error. + * Unfortunately, CFSocket offers no feedback on its errors. +**/ +- (NSError *)getSocketError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketCFSocketError", + @"AsyncSocket", [NSBundle mainBundle], + @"General CFSocket error", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketCFSocketError userInfo:info]; +} + +- (NSError *)getStreamError +{ + CFStreamError err; + if (theReadStream != NULL) + { + err = CFReadStreamGetError (theReadStream); + if (err.error != 0) return [self errorFromCFStreamError: err]; + } + + if (theWriteStream != NULL) + { + err = CFWriteStreamGetError (theWriteStream); + if (err.error != 0) return [self errorFromCFStreamError: err]; + } + + return nil; +} + +/** + * Returns a standard AsyncSocket abort error. +**/ +- (NSError *)getAbortError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketCanceledError", + @"AsyncSocket", [NSBundle mainBundle], + @"Connection canceled", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketCanceledError userInfo:info]; +} + +/** + * Returns a standard AsyncSocket connect timeout error. +**/ +- (NSError *)getConnectTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketConnectTimeoutError", + @"AsyncSocket", [NSBundle mainBundle], + @"Attempt to connect to host timed out", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketConnectTimeoutError userInfo:info]; +} + +/** + * Returns a standard AsyncSocket maxed out error. +**/ +- (NSError *)getReadMaxedOutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketReadMaxedOutError", + @"AsyncSocket", [NSBundle mainBundle], + @"Read operation reached set maximum length", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketReadMaxedOutError userInfo:info]; +} + +/** + * Returns a standard AsyncSocket read timeout error. +**/ +- (NSError *)getReadTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketReadTimeoutError", + @"AsyncSocket", [NSBundle mainBundle], + @"Read operation timed out", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketReadTimeoutError userInfo:info]; +} + +/** + * Returns a standard AsyncSocket write timeout error. +**/ +- (NSError *)getWriteTimeoutError +{ + NSString *errMsg = NSLocalizedStringWithDefaultValue(@"AsyncSocketWriteTimeoutError", + @"AsyncSocket", [NSBundle mainBundle], + @"Write operation timed out", nil); + + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncSocketErrorDomain code:AsyncSocketWriteTimeoutError userInfo:info]; +} + +- (NSError *)errorFromCFStreamError:(CFStreamError)err +{ + if (err.domain == 0 && err.error == 0) return nil; + + // Can't use switch; these constants aren't int literals. + NSString *domain = @"CFStreamError (unlisted domain)"; + NSString *message = nil; + + if(err.domain == kCFStreamErrorDomainPOSIX) { + domain = NSPOSIXErrorDomain; + } + else if(err.domain == kCFStreamErrorDomainMacOSStatus) { + domain = NSOSStatusErrorDomain; + } + else if(err.domain == kCFStreamErrorDomainMach) { + domain = NSMachErrorDomain; + } + else if(err.domain == kCFStreamErrorDomainNetDB) + { + domain = @"kCFStreamErrorDomainNetDB"; + message = [NSString stringWithCString:gai_strerror(err.error) encoding:NSASCIIStringEncoding]; + } + else if(err.domain == kCFStreamErrorDomainNetServices) { + domain = @"kCFStreamErrorDomainNetServices"; + } + else if(err.domain == kCFStreamErrorDomainSOCKS) { + domain = @"kCFStreamErrorDomainSOCKS"; + } + else if(err.domain == kCFStreamErrorDomainSystemConfiguration) { + domain = @"kCFStreamErrorDomainSystemConfiguration"; + } + else if(err.domain == kCFStreamErrorDomainSSL) { + domain = @"kCFStreamErrorDomainSSL"; + } + + NSDictionary *info = nil; + if(message != nil) + { + info = [NSDictionary dictionaryWithObject:message forKey:NSLocalizedDescriptionKey]; + } + return [NSError errorWithDomain:domain code:err.error userInfo:info]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Diagnostics +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)isDisconnected +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + if (theNativeSocket4 > 0) return NO; + if (theNativeSocket6 > 0) return NO; + + if (theSocket4) return NO; + if (theSocket6) return NO; + + if (theReadStream) return NO; + if (theWriteStream) return NO; + + return YES; +} + +- (BOOL)isConnected +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + return [self areStreamsConnected]; +} + +- (NSString *)connectedHost +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + if(theSocket4) + return [self connectedHostFromCFSocket4:theSocket4]; + if(theSocket6) + return [self connectedHostFromCFSocket6:theSocket6]; + + if(theNativeSocket4 > 0) + return [self connectedHostFromNativeSocket4:theNativeSocket4]; + if(theNativeSocket6 > 0) + return [self connectedHostFromNativeSocket6:theNativeSocket6]; + + return nil; +} + +- (UInt16)connectedPort +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + if(theSocket4) + return [self connectedPortFromCFSocket4:theSocket4]; + if(theSocket6) + return [self connectedPortFromCFSocket6:theSocket6]; + + if(theNativeSocket4 > 0) + return [self connectedPortFromNativeSocket4:theNativeSocket4]; + if(theNativeSocket6 > 0) + return [self connectedPortFromNativeSocket6:theNativeSocket6]; + + return 0; +} + +- (NSString *)localHost +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + if(theSocket4) + return [self localHostFromCFSocket4:theSocket4]; + if(theSocket6) + return [self localHostFromCFSocket6:theSocket6]; + + if(theNativeSocket4 > 0) + return [self localHostFromNativeSocket4:theNativeSocket4]; + if(theNativeSocket6 > 0) + return [self localHostFromNativeSocket6:theNativeSocket6]; + + return nil; +} + +- (UInt16)localPort +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + if(theSocket4) + return [self localPortFromCFSocket4:theSocket4]; + if(theSocket6) + return [self localPortFromCFSocket6:theSocket6]; + + if(theNativeSocket4 > 0) + return [self localPortFromNativeSocket4:theNativeSocket4]; + if(theNativeSocket6 > 0) + return [self localPortFromNativeSocket6:theNativeSocket6]; + + return 0; +} + +- (NSString *)connectedHost4 +{ + if(theSocket4) + return [self connectedHostFromCFSocket4:theSocket4]; + if(theNativeSocket4 > 0) + return [self connectedHostFromNativeSocket4:theNativeSocket4]; + + return nil; +} + +- (NSString *)connectedHost6 +{ + if(theSocket6) + return [self connectedHostFromCFSocket6:theSocket6]; + if(theNativeSocket6 > 0) + return [self connectedHostFromNativeSocket6:theNativeSocket6]; + + return nil; +} + +- (UInt16)connectedPort4 +{ + if(theSocket4) + return [self connectedPortFromCFSocket4:theSocket4]; + if(theNativeSocket4 > 0) + return [self connectedPortFromNativeSocket4:theNativeSocket4]; + + return 0; +} + +- (UInt16)connectedPort6 +{ + if(theSocket6) + return [self connectedPortFromCFSocket6:theSocket6]; + if(theNativeSocket6 > 0) + return [self connectedPortFromNativeSocket6:theNativeSocket6]; + + return 0; +} + +- (NSString *)localHost4 +{ + if(theSocket4) + return [self localHostFromCFSocket4:theSocket4]; + if(theNativeSocket4 > 0) + return [self localHostFromNativeSocket4:theNativeSocket4]; + + return nil; +} + +- (NSString *)localHost6 +{ + if(theSocket6) + return [self localHostFromCFSocket6:theSocket6]; + if(theNativeSocket6 > 0) + return [self localHostFromNativeSocket6:theNativeSocket6]; + + return nil; +} + +- (UInt16)localPort4 +{ + if(theSocket4) + return [self localPortFromCFSocket4:theSocket4]; + if(theNativeSocket4 > 0) + return [self localPortFromNativeSocket4:theNativeSocket4]; + + return 0; +} + +- (UInt16)localPort6 +{ + if(theSocket6) + return [self localPortFromCFSocket6:theSocket6]; + if(theNativeSocket6 > 0) + return [self localPortFromNativeSocket6:theNativeSocket6]; + + return 0; +} + +- (NSString *)connectedHostFromNativeSocket4:(CFSocketNativeHandle)theNativeSocket +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if(getpeername(theNativeSocket, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [self hostFromAddress4:&sockaddr4]; +} + +- (NSString *)connectedHostFromNativeSocket6:(CFSocketNativeHandle)theNativeSocket +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if(getpeername(theNativeSocket, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [self hostFromAddress6:&sockaddr6]; +} + +- (NSString *)connectedHostFromCFSocket4:(CFSocketRef)theSocket +{ + CFDataRef peeraddr; + NSString *peerstr = nil; + + if((peeraddr = CFSocketCopyPeerAddress(theSocket))) + { + struct sockaddr_in *pSockAddr = (struct sockaddr_in *)CFDataGetBytePtr(peeraddr); + + peerstr = [self hostFromAddress4:pSockAddr]; + CFRelease (peeraddr); + } + + return peerstr; +} + +- (NSString *)connectedHostFromCFSocket6:(CFSocketRef)theSocket +{ + CFDataRef peeraddr; + NSString *peerstr = nil; + + if((peeraddr = CFSocketCopyPeerAddress(theSocket))) + { + struct sockaddr_in6 *pSockAddr = (struct sockaddr_in6 *)CFDataGetBytePtr(peeraddr); + + peerstr = [self hostFromAddress6:pSockAddr]; + CFRelease (peeraddr); + } + + return peerstr; +} + +- (UInt16)connectedPortFromNativeSocket4:(CFSocketNativeHandle)theNativeSocket +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if(getpeername(theNativeSocket, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [self portFromAddress4:&sockaddr4]; +} + +- (UInt16)connectedPortFromNativeSocket6:(CFSocketNativeHandle)theNativeSocket +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if(getpeername(theNativeSocket, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [self portFromAddress6:&sockaddr6]; +} + +- (UInt16)connectedPortFromCFSocket4:(CFSocketRef)theSocket +{ + CFDataRef peeraddr; + UInt16 peerport = 0; + + if((peeraddr = CFSocketCopyPeerAddress(theSocket))) + { + struct sockaddr_in *pSockAddr = (struct sockaddr_in *)CFDataGetBytePtr(peeraddr); + + peerport = [self portFromAddress4:pSockAddr]; + CFRelease (peeraddr); + } + + return peerport; +} + +- (UInt16)connectedPortFromCFSocket6:(CFSocketRef)theSocket +{ + CFDataRef peeraddr; + UInt16 peerport = 0; + + if((peeraddr = CFSocketCopyPeerAddress(theSocket))) + { + struct sockaddr_in6 *pSockAddr = (struct sockaddr_in6 *)CFDataGetBytePtr(peeraddr); + + peerport = [self portFromAddress6:pSockAddr]; + CFRelease (peeraddr); + } + + return peerport; +} + +- (NSString *)localHostFromNativeSocket4:(CFSocketNativeHandle)theNativeSocket +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if(getsockname(theNativeSocket, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + return [self hostFromAddress4:&sockaddr4]; +} + +- (NSString *)localHostFromNativeSocket6:(CFSocketNativeHandle)theNativeSocket +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if(getsockname(theNativeSocket, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + return [self hostFromAddress6:&sockaddr6]; +} + +- (NSString *)localHostFromCFSocket4:(CFSocketRef)theSocket +{ + CFDataRef selfaddr; + NSString *selfstr = nil; + + if((selfaddr = CFSocketCopyAddress(theSocket))) + { + struct sockaddr_in *pSockAddr = (struct sockaddr_in *)CFDataGetBytePtr(selfaddr); + + selfstr = [self hostFromAddress4:pSockAddr]; + CFRelease (selfaddr); + } + + return selfstr; +} + +- (NSString *)localHostFromCFSocket6:(CFSocketRef)theSocket +{ + CFDataRef selfaddr; + NSString *selfstr = nil; + + if((selfaddr = CFSocketCopyAddress(theSocket))) + { + struct sockaddr_in6 *pSockAddr = (struct sockaddr_in6 *)CFDataGetBytePtr(selfaddr); + + selfstr = [self hostFromAddress6:pSockAddr]; + CFRelease (selfaddr); + } + + return selfstr; +} + +- (UInt16)localPortFromNativeSocket4:(CFSocketNativeHandle)theNativeSocket +{ + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if(getsockname(theNativeSocket, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + return [self portFromAddress4:&sockaddr4]; +} + +- (UInt16)localPortFromNativeSocket6:(CFSocketNativeHandle)theNativeSocket +{ + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if(getsockname(theNativeSocket, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + return [self portFromAddress6:&sockaddr6]; +} + +- (UInt16)localPortFromCFSocket4:(CFSocketRef)theSocket +{ + CFDataRef selfaddr; + UInt16 selfport = 0; + + if ((selfaddr = CFSocketCopyAddress(theSocket))) + { + struct sockaddr_in *pSockAddr = (struct sockaddr_in *)CFDataGetBytePtr(selfaddr); + + selfport = [self portFromAddress4:pSockAddr]; + CFRelease (selfaddr); + } + + return selfport; +} + +- (UInt16)localPortFromCFSocket6:(CFSocketRef)theSocket +{ + CFDataRef selfaddr; + UInt16 selfport = 0; + + if ((selfaddr = CFSocketCopyAddress(theSocket))) + { + struct sockaddr_in6 *pSockAddr = (struct sockaddr_in6 *)CFDataGetBytePtr(selfaddr); + + selfport = [self portFromAddress6:pSockAddr]; + CFRelease (selfaddr); + } + + return selfport; +} + +- (NSString *)hostFromAddress4:(struct sockaddr_in *)pSockaddr4 +{ + char addrBuf[INET_ADDRSTRLEN]; + + if(inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + [NSException raise:NSInternalInconsistencyException format:@"Cannot convert IPv4 address to string."]; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + +- (NSString *)hostFromAddress6:(struct sockaddr_in6 *)pSockaddr6 +{ + char addrBuf[INET6_ADDRSTRLEN]; + + if(inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) + { + [NSException raise:NSInternalInconsistencyException format:@"Cannot convert IPv6 address to string."]; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + +- (UInt16)portFromAddress4:(struct sockaddr_in *)pSockaddr4 +{ + return ntohs(pSockaddr4->sin_port); +} + +- (UInt16)portFromAddress6:(struct sockaddr_in6 *)pSockaddr6 +{ + return ntohs(pSockaddr6->sin6_port); +} + +- (NSData *)connectedAddress +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + // Extract address from CFSocket + + CFSocketRef theSocket; + + if (theSocket4) + theSocket = theSocket4; + else + theSocket = theSocket6; + + if (theSocket) + { + CFDataRef peeraddr = CFSocketCopyPeerAddress(theSocket); + + if (peeraddr == NULL) return nil; + + NSData *result = (__bridge_transfer NSData *)peeraddr; + return result; + } + + // Extract address from CFSocketNativeHandle + + socklen_t sockaddrlen; + CFSocketNativeHandle theNativeSocket = 0; + + if (theNativeSocket4 > 0) + { + theNativeSocket = theNativeSocket4; + sockaddrlen = sizeof(struct sockaddr_in); + } + else + { + theNativeSocket = theNativeSocket6; + sockaddrlen = sizeof(struct sockaddr_in6); + } + + NSData *result = nil; + void *sockaddr = malloc(sockaddrlen); + + if(getpeername(theNativeSocket, (struct sockaddr *)sockaddr, &sockaddrlen) >= 0) + { + result = [NSData dataWithBytesNoCopy:sockaddr length:sockaddrlen freeWhenDone:YES]; + } + else + { + free(sockaddr); + } + + return result; +} + +- (NSData *)localAddress +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + // Extract address from CFSocket + + CFSocketRef theSocket; + + if (theSocket4) + theSocket = theSocket4; + else + theSocket = theSocket6; + + if (theSocket) + { + CFDataRef selfaddr = CFSocketCopyAddress(theSocket); + + if (selfaddr == NULL) return nil; + + NSData *result = (__bridge_transfer NSData *)selfaddr; + return result; + } + + // Extract address from CFSocketNativeHandle + + socklen_t sockaddrlen; + CFSocketNativeHandle theNativeSocket = 0; + + if (theNativeSocket4 > 0) + { + theNativeSocket = theNativeSocket4; + sockaddrlen = sizeof(struct sockaddr_in); + } + else + { + theNativeSocket = theNativeSocket6; + sockaddrlen = sizeof(struct sockaddr_in6); + } + + NSData *result = nil; + void *sockaddr = malloc(sockaddrlen); + + if(getsockname(theNativeSocket, (struct sockaddr *)sockaddr, &sockaddrlen) >= 0) + { + result = [NSData dataWithBytesNoCopy:sockaddr length:sockaddrlen freeWhenDone:YES]; + } + else + { + free(sockaddr); + } + + return result; +} + +- (BOOL)isIPv4 +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + return (theNativeSocket4 > 0 || theSocket4 != NULL); +} + +- (BOOL)isIPv6 +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + return (theNativeSocket6 > 0 || theSocket6 != NULL); +} + +- (BOOL)areStreamsConnected +{ + CFStreamStatus s; + + if (theReadStream != NULL) + { + s = CFReadStreamGetStatus(theReadStream); + if ( !(s == kCFStreamStatusOpen || s == kCFStreamStatusReading || s == kCFStreamStatusError) ) + return NO; + } + else return NO; + + if (theWriteStream != NULL) + { + s = CFWriteStreamGetStatus(theWriteStream); + if ( !(s == kCFStreamStatusOpen || s == kCFStreamStatusWriting || s == kCFStreamStatusError) ) + return NO; + } + else return NO; + + return YES; +} + +- (NSString *)description +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + static const char *statstr[] = {"not open","opening","open","reading","writing","at end","closed","has error"}; + CFStreamStatus rs = (theReadStream != NULL) ? CFReadStreamGetStatus(theReadStream) : 0; + CFStreamStatus ws = (theWriteStream != NULL) ? CFWriteStreamGetStatus(theWriteStream) : 0; + + NSString *peerstr, *selfstr; + + BOOL is4 = [self isIPv4]; + BOOL is6 = [self isIPv6]; + + if (is4 || is6) + { + if (is4 && is6) + { + peerstr = [NSString stringWithFormat: @"%@/%@ %u", + [self connectedHost4], + [self connectedHost6], + [self connectedPort]]; + } + else if (is4) + { + peerstr = [NSString stringWithFormat: @"%@ %u", + [self connectedHost4], + [self connectedPort4]]; + } + else + { + peerstr = [NSString stringWithFormat: @"%@ %u", + [self connectedHost6], + [self connectedPort6]]; + } + } + else peerstr = @"nowhere"; + + if (is4 || is6) + { + if (is4 && is6) + { + selfstr = [NSString stringWithFormat: @"%@/%@ %u", + [self localHost4], + [self localHost6], + [self localPort]]; + } + else if (is4) + { + selfstr = [NSString stringWithFormat: @"%@ %u", + [self localHost4], + [self localPort4]]; + } + else + { + selfstr = [NSString stringWithFormat: @"%@ %u", + [self localHost6], + [self localPort6]]; + } + } + else selfstr = @"nowhere"; + + NSMutableString *ms = [[NSMutableString alloc] initWithCapacity:150]; + + [ms appendString:[NSString stringWithFormat:@"readLength > 0) + percentDone = (float)theCurrentRead->bytesDone / (float)theCurrentRead->readLength * 100.0F; + else + percentDone = 100.0F; + + [ms appendString: [NSString stringWithFormat:@"currently read %u bytes (%d%% done), ", + (unsigned int)[theCurrentRead->buffer length], + theCurrentRead->bytesDone ? percentDone : 0]]; + } + + if (theCurrentWrite == nil || [theCurrentWrite isKindOfClass:[AsyncSpecialPacket class]]) + [ms appendString: @"no current write, "]; + else + { + int percentDone = (float)theCurrentWrite->bytesDone / (float)[theCurrentWrite->buffer length] * 100.0F; + + [ms appendString: [NSString stringWithFormat:@"currently written %u (%d%%), ", + (unsigned int)[theCurrentWrite->buffer length], + theCurrentWrite->bytesDone ? percentDone : 0]]; + } + + [ms appendString:[NSString stringWithFormat:@"read stream %p %s, ", theReadStream, statstr[rs]]]; + [ms appendString:[NSString stringWithFormat:@"write stream %p %s", theWriteStream, statstr[ws]]]; + + if(theFlags & kDisconnectAfterReads) + { + if(theFlags & kDisconnectAfterWrites) + [ms appendString: @", will disconnect after reads & writes"]; + else + [ms appendString: @", will disconnect after reads"]; + } + else if(theFlags & kDisconnectAfterWrites) + { + [ms appendString: @", will disconnect after writes"]; + } + + if (![self isConnected]) [ms appendString: @", not connected"]; + + [ms appendString:@">"]; + + return ms; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Reading +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + if (offset > [buffer length]) return; + if (theFlags & kForbidReadsWrites) return; + + AsyncReadPacket *packet = [[AsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:length + timeout:timeout + readLength:0 + terminator:nil + tag:tag]; + [theReadQueue addObject:packet]; + [self scheduleDequeueRead]; + +} + +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag]; +} + +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + if (length == 0) return; + if (offset > [buffer length]) return; + if (theFlags & kForbidReadsWrites) return; + + AsyncReadPacket *packet = [[AsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:0 + timeout:timeout + readLength:length + terminator:nil + tag:tag]; + [theReadQueue addObject:packet]; + [self scheduleDequeueRead]; + +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag +{ + [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:length tag:tag]; +} + +- (void)readDataToData:(NSData *)data + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + if (data == nil || [data length] == 0) return; + if (offset > [buffer length]) return; + if (length > 0 && length < [data length]) return; + if (theFlags & kForbidReadsWrites) return; + + AsyncReadPacket *packet = [[AsyncReadPacket alloc] initWithData:buffer + startOffset:offset + maxLength:length + timeout:timeout + readLength:0 + terminator:data + tag:tag]; + [theReadQueue addObject:packet]; + [self scheduleDequeueRead]; + +} + +/** + * Puts a maybeDequeueRead on the run loop. + * An assumption here is that selectors will be performed consecutively within their priority. +**/ +- (void)scheduleDequeueRead +{ + if((theFlags & kDequeueReadScheduled) == 0) + { + theFlags |= kDequeueReadScheduled; + [self performSelector:@selector(maybeDequeueRead) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + } +} + +/** + * This method starts a new read, if needed. + * It is called when a user requests a read, + * or when a stream opens that may have requested reads sitting in the queue, etc. +**/ +- (void)maybeDequeueRead +{ + // Unset the flag indicating a call to this method is scheduled + theFlags &= ~kDequeueReadScheduled; + + // If we're not currently processing a read AND we have an available read stream + if((theCurrentRead == nil) && (theReadStream != NULL)) + { + if([theReadQueue count] > 0) + { + // Dequeue the next object in the write queue + theCurrentRead = [theReadQueue objectAtIndex:0]; + [theReadQueue removeObjectAtIndex:0]; + + if([theCurrentRead isKindOfClass:[AsyncSpecialPacket class]]) + { + // Attempt to start TLS + theFlags |= kStartingReadTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + // Start time-out timer + if(theCurrentRead->timeout >= 0.0) + { + theReadTimer = [NSTimer timerWithTimeInterval:theCurrentRead->timeout + target:self + selector:@selector(doReadTimeout:) + userInfo:nil + repeats:NO]; + [self runLoopAddTimer:theReadTimer]; + } + + // Immediately read, if possible + [self doBytesAvailable]; + } + } + else if(theFlags & kDisconnectAfterReads) + { + if(theFlags & kDisconnectAfterWrites) + { + if(([theWriteQueue count] == 0) && (theCurrentWrite == nil)) + { + [self disconnect]; + } + } + else + { + [self disconnect]; + } + } + } +} + +/** + * Call this method in doBytesAvailable instead of CFReadStreamHasBytesAvailable(). + * This method supports pre-buffering properly as well as the kSocketHasBytesAvailable flag. +**/ +- (BOOL)hasBytesAvailable +{ + if ((theFlags & kSocketHasBytesAvailable) || ([partialReadBuffer length] > 0)) + { + return YES; + } + else + { + return CFReadStreamHasBytesAvailable(theReadStream); + } +} + +/** + * Call this method in doBytesAvailable instead of CFReadStreamRead(). + * This method support pre-buffering properly. +**/ +- (CFIndex)readIntoBuffer:(void *)buffer maxLength:(NSUInteger)length +{ + if([partialReadBuffer length] > 0) + { + // Determine the maximum amount of data to read + NSUInteger bytesToRead = MIN(length, [partialReadBuffer length]); + + // Copy the bytes from the partial read buffer + memcpy(buffer, [partialReadBuffer bytes], (size_t)bytesToRead); + + // Remove the copied bytes from the partial read buffer + [partialReadBuffer replaceBytesInRange:NSMakeRange(0, bytesToRead) withBytes:NULL length:0]; + + return (CFIndex)bytesToRead; + } + else + { + // Unset the "has-bytes-available" flag + theFlags &= ~kSocketHasBytesAvailable; + + return CFReadStreamRead(theReadStream, (UInt8 *)buffer, length); + } +} + +/** + * This method is called when a new read is taken from the read queue or when new data becomes available on the stream. +**/ +- (void)doBytesAvailable +{ + // If data is available on the stream, but there is no read request, then we don't need to process the data yet. + // Also, if there is a read request but no read stream setup, we can't process any data yet. + if((theCurrentRead == nil) || (theReadStream == NULL)) + { + return; + } + + // Note: This method is not called if theCurrentRead is an AsyncSpecialPacket (startTLS packet) + + NSUInteger totalBytesRead = 0; + + BOOL done = NO; + BOOL socketError = NO; + BOOL maxoutError = NO; + + while(!done && !socketError && !maxoutError && [self hasBytesAvailable]) + { + BOOL didPreBuffer = NO; + BOOL didReadFromPreBuffer = NO; + + // There are 3 types of read packets: + // + // 1) Read all available data. + // 2) Read a specific length of data. + // 3) Read up to a particular terminator. + + NSUInteger bytesToRead; + + if (theCurrentRead->term != nil) + { + // Read type #3 - read up to a terminator + // + // If pre-buffering is enabled we'll read a chunk and search for the terminator. + // If the terminator is found, overflow data will be placed in the partialReadBuffer for the next read. + // + // If pre-buffering is disabled we'll be forced to read only a few bytes. + // Just enough to ensure we don't go past our term or over our max limit. + // + // If we already have data pre-buffered, we can read directly from it. + + if ([partialReadBuffer length] > 0) + { + didReadFromPreBuffer = YES; + bytesToRead = [theCurrentRead readLengthForTermWithPreBuffer:partialReadBuffer found:&done]; + } + else + { + if (theFlags & kEnablePreBuffering) + { + didPreBuffer = YES; + bytesToRead = [theCurrentRead prebufferReadLengthForTerm]; + } + else + { + bytesToRead = [theCurrentRead readLengthForTerm]; + } + } + } + else + { + // Read type #1 or #2 + + bytesToRead = [theCurrentRead readLengthForNonTerm]; + } + + // Make sure we have enough room in the buffer for our read + + NSUInteger buffSize = [theCurrentRead->buffer length]; + NSUInteger buffSpace = buffSize - theCurrentRead->startOffset - theCurrentRead->bytesDone; + + if (bytesToRead > buffSpace) + { + NSUInteger buffInc = bytesToRead - buffSpace; + + [theCurrentRead->buffer increaseLengthBy:buffInc]; + } + + // Read data into packet buffer + + void *buffer = [theCurrentRead->buffer mutableBytes] + theCurrentRead->startOffset; + void *subBuffer = buffer + theCurrentRead->bytesDone; + + CFIndex result = [self readIntoBuffer:subBuffer maxLength:bytesToRead]; + + // Check results + if (result < 0) + { + socketError = YES; + } + else + { + CFIndex bytesRead = result; + + // Update total amount read for the current read + theCurrentRead->bytesDone += bytesRead; + + // Update total amount read in this method invocation + totalBytesRead += bytesRead; + + + // Is packet done? + if (theCurrentRead->readLength > 0) + { + // Read type #2 - read a specific length of data + + done = (theCurrentRead->bytesDone == theCurrentRead->readLength); + } + else if (theCurrentRead->term != nil) + { + // Read type #3 - read up to a terminator + + if (didPreBuffer) + { + // Search for the terminating sequence within the big chunk we just read. + + NSInteger overflow = [theCurrentRead searchForTermAfterPreBuffering:result]; + + if (overflow > 0) + { + // Copy excess data into partialReadBuffer + void *overflowBuffer = buffer + theCurrentRead->bytesDone - overflow; + + [partialReadBuffer appendBytes:overflowBuffer length:overflow]; + + // Update the bytesDone variable. + theCurrentRead->bytesDone -= overflow; + + // Note: The completeCurrentRead method will trim the buffer for us. + } + + done = (overflow >= 0); + } + else if (didReadFromPreBuffer) + { + // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method + } + else + { + // Search for the terminating sequence at the end of the buffer + + NSUInteger termlen = [theCurrentRead->term length]; + + if(theCurrentRead->bytesDone >= termlen) + { + void *bufferEnd = buffer + (theCurrentRead->bytesDone - termlen); + + const void *seq = [theCurrentRead->term bytes]; + + done = (memcmp (bufferEnd, seq, termlen) == 0); + } + } + + if(!done && theCurrentRead->maxLength > 0) + { + // We're not done and there's a set maxLength. + // Have we reached that maxLength yet? + + if(theCurrentRead->bytesDone >= theCurrentRead->maxLength) + { + maxoutError = YES; + } + } + } + else + { + // Read type #1 - read all available data + // + // We're done when: + // - we reach maxLength (if there is a max) + // - all readable is read (see below) + + if (theCurrentRead->maxLength > 0) + { + done = (theCurrentRead->bytesDone >= theCurrentRead->maxLength); + } + } + } + } + + if (theCurrentRead->readLength <= 0 && theCurrentRead->term == nil) + { + // Read type #1 - read all available data + + if (theCurrentRead->bytesDone > 0) + { + // Ran out of bytes, so the "read-all-available-data" type packet is done + done = YES; + } + } + + if (done) + { + [self completeCurrentRead]; + if (!socketError) [self scheduleDequeueRead]; + } + else if (totalBytesRead > 0) + { + // We're not done with the readToLength or readToData yet, but we have read in some bytes + if ([theDelegate respondsToSelector:@selector(onSocket:didReadPartialDataOfLength:tag:)]) + { + [theDelegate onSocket:self didReadPartialDataOfLength:totalBytesRead tag:theCurrentRead->tag]; + } + } + + if(socketError) + { + CFStreamError err = CFReadStreamGetError(theReadStream); + [self closeWithError:[self errorFromCFStreamError:err]]; + return; + } + + if(maxoutError) + { + [self closeWithError:[self getReadMaxedOutError]]; + return; + } +} + +// Ends current read and calls delegate. +- (void)completeCurrentRead +{ + NSAssert(theCurrentRead, @"Trying to complete current read when there is no current read."); + + NSData *result; + + if (theCurrentRead->bufferOwner) + { + // We created the buffer on behalf of the user. + // Trim our buffer to be the proper size. + [theCurrentRead->buffer setLength:theCurrentRead->bytesDone]; + + result = theCurrentRead->buffer; + } + else + { + // We did NOT create the buffer. + // The buffer is owned by the caller. + // Only trim the buffer if we had to increase its size. + + if ([theCurrentRead->buffer length] > theCurrentRead->originalBufferLength) + { + NSUInteger readSize = theCurrentRead->startOffset + theCurrentRead->bytesDone; + NSUInteger origSize = theCurrentRead->originalBufferLength; + + NSUInteger buffSize = MAX(readSize, origSize); + + [theCurrentRead->buffer setLength:buffSize]; + } + + void *buffer = [theCurrentRead->buffer mutableBytes] + theCurrentRead->startOffset; + + result = [NSData dataWithBytesNoCopy:buffer length:theCurrentRead->bytesDone freeWhenDone:NO]; + } + + if([theDelegate respondsToSelector:@selector(onSocket:didReadData:withTag:)]) + { + [theDelegate onSocket:self didReadData:result withTag:theCurrentRead->tag]; + } + + // Caller may have disconnected in the above delegate method + if (theCurrentRead != nil) + { + [self endCurrentRead]; + } +} + +// Ends current read. +- (void)endCurrentRead +{ + NSAssert(theCurrentRead, @"Trying to end current read when there is no current read."); + + [theReadTimer invalidate]; + theReadTimer = nil; + + theCurrentRead = nil; +} + +- (void)doReadTimeout:(NSTimer *)timer +{ + #pragma unused(timer) + + NSTimeInterval timeoutExtension = 0.0; + + if([theDelegate respondsToSelector:@selector(onSocket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) + { + timeoutExtension = [theDelegate onSocket:self shouldTimeoutReadWithTag:theCurrentRead->tag + elapsed:theCurrentRead->timeout + bytesDone:theCurrentRead->bytesDone]; + } + + if(timeoutExtension > 0.0) + { + theCurrentRead->timeout += timeoutExtension; + + theReadTimer = [NSTimer timerWithTimeInterval:timeoutExtension + target:self + selector:@selector(doReadTimeout:) + userInfo:nil + repeats:NO]; + [self runLoopAddTimer:theReadTimer]; + } + else + { + // Do not call endCurrentRead here. + // We must allow the delegate access to any partial read in the unreadData method. + + [self closeWithError:[self getReadTimeoutError]]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Writing +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + if (data == nil || [data length] == 0) return; + if (theFlags & kForbidReadsWrites) return; + + AsyncWritePacket *packet = [[AsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; + + [theWriteQueue addObject:packet]; + [self scheduleDequeueWrite]; + +} + +- (void)scheduleDequeueWrite +{ + if((theFlags & kDequeueWriteScheduled) == 0) + { + theFlags |= kDequeueWriteScheduled; + [self performSelector:@selector(maybeDequeueWrite) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + } +} + +/** + * Conditionally starts a new write. + * + * IF there is not another write in process + * AND there is a write queued + * AND we have a write stream available + * + * This method also handles auto-disconnect post read/write completion. +**/ +- (void)maybeDequeueWrite +{ + // Unset the flag indicating a call to this method is scheduled + theFlags &= ~kDequeueWriteScheduled; + + // If we're not currently processing a write AND we have an available write stream + if((theCurrentWrite == nil) && (theWriteStream != NULL)) + { + if([theWriteQueue count] > 0) + { + // Dequeue the next object in the write queue + theCurrentWrite = [theWriteQueue objectAtIndex:0]; + [theWriteQueue removeObjectAtIndex:0]; + + if([theCurrentWrite isKindOfClass:[AsyncSpecialPacket class]]) + { + // Attempt to start TLS + theFlags |= kStartingWriteTLS; + + // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set + [self maybeStartTLS]; + } + else + { + // Start time-out timer + if(theCurrentWrite->timeout >= 0.0) + { + theWriteTimer = [NSTimer timerWithTimeInterval:theCurrentWrite->timeout + target:self + selector:@selector(doWriteTimeout:) + userInfo:nil + repeats:NO]; + [self runLoopAddTimer:theWriteTimer]; + } + + // Immediately write, if possible + [self doSendBytes]; + } + } + else if(theFlags & kDisconnectAfterWrites) + { + if(theFlags & kDisconnectAfterReads) + { + if(([theReadQueue count] == 0) && (theCurrentRead == nil)) + { + [self disconnect]; + } + } + else + { + [self disconnect]; + } + } + } +} + +/** + * Call this method in doSendBytes instead of CFWriteStreamCanAcceptBytes(). + * This method supports the kSocketCanAcceptBytes flag. +**/ +- (BOOL)canAcceptBytes +{ + if (theFlags & kSocketCanAcceptBytes) + { + return YES; + } + else + { + return CFWriteStreamCanAcceptBytes(theWriteStream); + } +} + +- (void)doSendBytes +{ + if ((theCurrentWrite == nil) || (theWriteStream == NULL)) + { + return; + } + + // Note: This method is not called if theCurrentWrite is an AsyncSpecialPacket (startTLS packet) + + NSUInteger totalBytesWritten = 0; + + BOOL done = NO; + BOOL error = NO; + + while (!done && !error && [self canAcceptBytes]) + { + // Figure out what to write + NSUInteger bytesRemaining = [theCurrentWrite->buffer length] - theCurrentWrite->bytesDone; + NSUInteger bytesToWrite = (bytesRemaining < WRITE_CHUNKSIZE) ? bytesRemaining : WRITE_CHUNKSIZE; + + UInt8 *writestart = (UInt8 *)([theCurrentWrite->buffer bytes] + theCurrentWrite->bytesDone); + + // Write + CFIndex result = CFWriteStreamWrite(theWriteStream, writestart, bytesToWrite); + + // Unset the "can accept bytes" flag + theFlags &= ~kSocketCanAcceptBytes; + + // Check results + if (result < 0) + { + error = YES; + } + else + { + CFIndex bytesWritten = result; + + // Update total amount read for the current write + theCurrentWrite->bytesDone += bytesWritten; + + // Update total amount written in this method invocation + totalBytesWritten += bytesWritten; + + // Is packet done? + done = ([theCurrentWrite->buffer length] == theCurrentWrite->bytesDone); + } + } + + if(done) + { + [self completeCurrentWrite]; + [self scheduleDequeueWrite]; + } + else if(error) + { + CFStreamError err = CFWriteStreamGetError(theWriteStream); + [self closeWithError:[self errorFromCFStreamError:err]]; + return; + } + else if (totalBytesWritten > 0) + { + // We're not done with the entire write, but we have written some bytes + if ([theDelegate respondsToSelector:@selector(onSocket:didWritePartialDataOfLength:tag:)]) + { + [theDelegate onSocket:self didWritePartialDataOfLength:totalBytesWritten tag:theCurrentWrite->tag]; + } + } +} + +// Ends current write and calls delegate. +- (void)completeCurrentWrite +{ + NSAssert(theCurrentWrite, @"Trying to complete current write when there is no current write."); + + if ([theDelegate respondsToSelector:@selector(onSocket:didWriteDataWithTag:)]) + { + [theDelegate onSocket:self didWriteDataWithTag:theCurrentWrite->tag]; + } + + if (theCurrentWrite != nil) [self endCurrentWrite]; // Caller may have disconnected. +} + +// Ends current write. +- (void)endCurrentWrite +{ + NSAssert(theCurrentWrite, @"Trying to complete current write when there is no current write."); + + [theWriteTimer invalidate]; + theWriteTimer = nil; + + theCurrentWrite = nil; +} + +- (void)doWriteTimeout:(NSTimer *)timer +{ + #pragma unused(timer) + + NSTimeInterval timeoutExtension = 0.0; + + if([theDelegate respondsToSelector:@selector(onSocket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) + { + timeoutExtension = [theDelegate onSocket:self shouldTimeoutWriteWithTag:theCurrentWrite->tag + elapsed:theCurrentWrite->timeout + bytesDone:theCurrentWrite->bytesDone]; + } + + if(timeoutExtension > 0.0) + { + theCurrentWrite->timeout += timeoutExtension; + + theWriteTimer = [NSTimer timerWithTimeInterval:timeoutExtension + target:self + selector:@selector(doWriteTimeout:) + userInfo:nil + repeats:NO]; + [self runLoopAddTimer:theWriteTimer]; + } + else + { + [self closeWithError:[self getWriteTimeoutError]]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Security +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)startTLS:(NSDictionary *)tlsSettings +{ +#if DEBUG_THREAD_SAFETY + [self checkForThreadSafety]; +#endif + + if(tlsSettings == nil) + { + // Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary, + // but causes problems if we later try to fetch the remote host's certificate. + // + // To be exact, it causes the following to return NULL instead of the normal result: + // CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates) + // + // So we use an empty dictionary instead, which works perfectly. + + tlsSettings = [NSDictionary dictionary]; + } + + AsyncSpecialPacket *packet = [[AsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings]; + + [theReadQueue addObject:packet]; + [self scheduleDequeueRead]; + + [theWriteQueue addObject:packet]; + [self scheduleDequeueWrite]; + +} + +- (void)maybeStartTLS +{ + // We can't start TLS until: + // - All queued reads prior to the user calling StartTLS are complete + // - All queued writes prior to the user calling StartTLS are complete + // + // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set + + if((theFlags & kStartingReadTLS) && (theFlags & kStartingWriteTLS)) + { + AsyncSpecialPacket *tlsPacket = (AsyncSpecialPacket *)theCurrentRead; + + BOOL didStartOnReadStream = CFReadStreamSetProperty(theReadStream, kCFStreamPropertySSLSettings, + (__bridge CFDictionaryRef)tlsPacket->tlsSettings); + BOOL didStartOnWriteStream = CFWriteStreamSetProperty(theWriteStream, kCFStreamPropertySSLSettings, + (__bridge CFDictionaryRef)tlsPacket->tlsSettings); + + if(!didStartOnReadStream || !didStartOnWriteStream) + { + [self closeWithError:[self getSocketError]]; + } + } +} + +- (void)onTLSHandshakeSuccessful +{ + if((theFlags & kStartingReadTLS) && (theFlags & kStartingWriteTLS)) + { + theFlags &= ~kStartingReadTLS; + theFlags &= ~kStartingWriteTLS; + + if([theDelegate respondsToSelector:@selector(onSocketDidSecure:)]) + { + [theDelegate onSocketDidSecure:self]; + } + + [self endCurrentRead]; + [self endCurrentWrite]; + + [self scheduleDequeueRead]; + [self scheduleDequeueWrite]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark CF Callbacks +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)doCFSocketCallback:(CFSocketCallBackType)type + forSocket:(CFSocketRef)sock + withAddress:(NSData *)address + withData:(const void *)pData +{ + #pragma unused(address) + + NSParameterAssert ((sock == theSocket4) || (sock == theSocket6)); + + switch (type) + { + case kCFSocketConnectCallBack: + // The data argument is either NULL or a pointer to an SInt32 error code, if the connect failed. + if(pData) + [self doSocketOpen:sock withCFSocketError:kCFSocketError]; + else + [self doSocketOpen:sock withCFSocketError:kCFSocketSuccess]; + break; + case kCFSocketAcceptCallBack: + [self doAcceptFromSocket:sock withNewNativeSocket:*((CFSocketNativeHandle *)pData)]; + break; + default: + NSLog(@"AsyncSocket %p received unexpected CFSocketCallBackType %i", self, (int)type); + break; + } +} + +- (void)doCFReadStreamCallback:(CFStreamEventType)type forStream:(CFReadStreamRef)stream +{ + #pragma unused(stream) + + NSParameterAssert(theReadStream != NULL); + + CFStreamError err; + switch (type) + { + case kCFStreamEventOpenCompleted: + theFlags |= kDidCompleteOpenForRead; + [self doStreamOpen]; + break; + case kCFStreamEventHasBytesAvailable: + if(theFlags & kStartingReadTLS) { + [self onTLSHandshakeSuccessful]; + } + else { + theFlags |= kSocketHasBytesAvailable; + [self doBytesAvailable]; + } + break; + case kCFStreamEventErrorOccurred: + case kCFStreamEventEndEncountered: + err = CFReadStreamGetError (theReadStream); + [self closeWithError: [self errorFromCFStreamError:err]]; + break; + default: + NSLog(@"AsyncSocket %p received unexpected CFReadStream callback, CFStreamEventType %i", self, (int)type); + } +} + +- (void)doCFWriteStreamCallback:(CFStreamEventType)type forStream:(CFWriteStreamRef)stream +{ + #pragma unused(stream) + + NSParameterAssert(theWriteStream != NULL); + + CFStreamError err; + switch (type) + { + case kCFStreamEventOpenCompleted: + theFlags |= kDidCompleteOpenForWrite; + [self doStreamOpen]; + break; + case kCFStreamEventCanAcceptBytes: + if(theFlags & kStartingWriteTLS) { + [self onTLSHandshakeSuccessful]; + } + else { + theFlags |= kSocketCanAcceptBytes; + [self doSendBytes]; + } + break; + case kCFStreamEventErrorOccurred: + case kCFStreamEventEndEncountered: + err = CFWriteStreamGetError (theWriteStream); + [self closeWithError: [self errorFromCFStreamError:err]]; + break; + default: + NSLog(@"AsyncSocket %p received unexpected CFWriteStream callback, CFStreamEventType %i", self, (int)type); + } +} + +/** + * This is the callback we setup for CFSocket. + * This method does nothing but forward the call to it's Objective-C counterpart +**/ +static void MyCFSocketCallback (CFSocketRef sref, CFSocketCallBackType type, CFDataRef inAddress, const void *pData, void *pInfo) +{ + @autoreleasepool { + + AsyncSocket *theSocket = (__bridge AsyncSocket *)pInfo; + NSData *address = [(__bridge NSData *)inAddress copy]; + + [theSocket doCFSocketCallback:type forSocket:sref withAddress:address withData:pData]; + + } +} + +/** + * This is the callback we setup for CFReadStream. + * This method does nothing but forward the call to it's Objective-C counterpart +**/ +static void MyCFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) +{ + @autoreleasepool { + + AsyncSocket *theSocket = (__bridge AsyncSocket *)pInfo; + [theSocket doCFReadStreamCallback:type forStream:stream]; + + } +} + +/** + * This is the callback we setup for CFWriteStream. + * This method does nothing but forward the call to it's Objective-C counterpart +**/ +static void MyCFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) +{ + @autoreleasepool { + + AsyncSocket *theSocket = (__bridge AsyncSocket *)pInfo; + [theSocket doCFWriteStreamCallback:type forStream:stream]; + + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Class Methods +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Return line separators. ++ (NSData *)CRLFData +{ + return [NSData dataWithBytes:"\x0D\x0A" length:2]; +} + ++ (NSData *)CRData +{ + return [NSData dataWithBytes:"\x0D" length:1]; +} + ++ (NSData *)LFData +{ + return [NSData dataWithBytes:"\x0A" length:1]; +} + ++ (NSData *)ZeroData +{ + return [NSData dataWithBytes:"" length:1]; +} + +@end diff --git a/Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncUdpSocket.h b/Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncUdpSocket.h new file mode 100644 index 0000000..7bacef4 --- /dev/null +++ b/Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncUdpSocket.h @@ -0,0 +1,368 @@ +// +// AsyncUdpSocket.h +// +// This class is in the public domain. +// Originally created by Robbie Hanson on Wed Oct 01 2008. +// Updated and maintained by Deusty Designs and the Mac development community. +// +// http://code.google.com/p/cocoaasyncsocket/ +// + +#import + +@class AsyncSendPacket; +@class AsyncReceivePacket; + +extern NSString *const AsyncUdpSocketException; +extern NSString *const AsyncUdpSocketErrorDomain; + +typedef NS_ENUM(NSInteger, AsyncUdpSocketError) { + AsyncUdpSocketCFSocketError = kCFSocketError, // From CFSocketError enum + AsyncUdpSocketNoError = 0, // Never used + AsyncUdpSocketBadParameter, // Used if given a bad parameter (such as an improper address) + AsyncUdpSocketIPv4Unavailable, // Used if you bind/connect using IPv6 only + AsyncUdpSocketIPv6Unavailable, // Used if you bind/connect using IPv4 only (or iPhone) + AsyncUdpSocketSendTimeoutError, + AsyncUdpSocketReceiveTimeoutError +}; + +@interface AsyncUdpSocket : NSObject +{ + CFSocketRef theSocket4; // IPv4 socket + CFSocketRef theSocket6; // IPv6 socket + + CFRunLoopSourceRef theSource4; // For theSocket4 + CFRunLoopSourceRef theSource6; // For theSocket6 + CFRunLoopRef theRunLoop; + CFSocketContext theContext; + NSArray *theRunLoopModes; + + NSMutableArray *theSendQueue; + AsyncSendPacket *theCurrentSend; + NSTimer *theSendTimer; + + NSMutableArray *theReceiveQueue; + AsyncReceivePacket *theCurrentReceive; + NSTimer *theReceiveTimer; + + id theDelegate; + UInt16 theFlags; + + long theUserData; + + NSString *cachedLocalHost; + UInt16 cachedLocalPort; + + NSString *cachedConnectedHost; + UInt16 cachedConnectedPort; + + UInt32 maxReceiveBufferSize; +} + +/** + * Creates new instances of AsyncUdpSocket. +**/ +- (id)init; +- (id)initWithDelegate:(id)delegate; +- (id)initWithDelegate:(id)delegate userData:(long)userData; + +/** + * Creates new instances of AsyncUdpSocket that support only IPv4 or IPv6. + * The other init methods will support both, unless specifically binded or connected to one protocol. + * If you know you'll only be using one protocol, these init methods may be a bit more efficient. +**/ +- (id)initIPv4; +- (id)initIPv6; + +- (id)delegate; +- (void)setDelegate:(id)delegate; + +- (long)userData; +- (void)setUserData:(long)userData; + +/** + * Returns the local address info for the socket. + * + * Note: Address info may not be available until after the socket has been bind'ed, + * or until after data has been sent. +**/ +- (NSString *)localHost; +- (UInt16)localPort; + +/** + * Returns the remote address info for the socket. + * + * Note: Since UDP is connectionless by design, connected address info + * will not be available unless the socket is explicitly connected to a remote host/port +**/ +- (NSString *)connectedHost; +- (UInt16)connectedPort; + +/** + * Returns whether or not this socket has been connected to a single host. + * By design, UDP is a connectionless protocol, and connecting is not needed. + * If connected, the socket will only be able to send/receive data to/from the connected host. +**/ +- (BOOL)isConnected; + +/** + * Returns whether or not this socket has been closed. + * The only way a socket can be closed is if you explicitly call one of the close methods. +**/ +- (BOOL)isClosed; + +/** + * Returns whether or not this socket supports IPv4. + * By default this will be true, unless the socket is specifically initialized as IPv6 only, + * or is binded or connected to an IPv6 address. +**/ +- (BOOL)isIPv4; + +/** + * Returns whether or not this socket supports IPv6. + * By default this will be true, unless the socket is specifically initialized as IPv4 only, + * or is binded or connected to an IPv4 address. + * + * This method will also return false on platforms that do not support IPv6. + * Note: The iPhone does not currently support IPv6. +**/ +- (BOOL)isIPv6; + +/** + * Returns the mtu of the socket. + * If unknown, returns zero. + * + * Sending data larger than this may result in an error. + * This is an advanced topic, and one should understand the wide range of mtu's on networks and the internet. + * Therefore this method is only for reference and may be of little use in many situations. +**/ +- (unsigned int)maximumTransmissionUnit; + +/** + * Binds the UDP socket to the given port and optional address. + * Binding should be done for server sockets that receive data prior to sending it. + * Client sockets can skip binding, + * as the OS will automatically assign the socket an available port when it starts sending data. + * + * You cannot bind a socket after its been connected. + * You can only bind a socket once. + * You can still connect a socket (if desired) after binding. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ +- (BOOL)bindToPort:(UInt16)port error:(NSError **)errPtr; +- (BOOL)bindToAddress:(NSString *)localAddr port:(UInt16)port error:(NSError **)errPtr; + +/** + * Connects the UDP socket to the given host and port. + * By design, UDP is a connectionless protocol, and connecting is not needed. + * + * Choosing to connect to a specific host/port has the following effect: + * - You will only be able to send data to the connected host/port. + * - You will only be able to receive data from the connected host/port. + * - You will receive ICMP messages that come from the connected host/port, such as "connection refused". + * + * Connecting a UDP socket does not result in any communication on the socket. + * It simply changes the internal state of the socket. + * + * You cannot bind a socket after its been connected. + * You can only connect a socket once. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ +- (BOOL)connectToHost:(NSString *)host onPort:(UInt16)port error:(NSError **)errPtr; +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; + +/** + * Join multicast group + * + * Group should be an IP address (eg @"225.228.0.1") +**/ +- (BOOL)joinMulticastGroup:(NSString *)group error:(NSError **)errPtr; +- (BOOL)joinMulticastGroup:(NSString *)group withAddress:(NSString *)interface error:(NSError **)errPtr; + +/** + * By default, the underlying socket in the OS will not allow you to send broadcast messages. + * In order to send broadcast messages, you need to enable this functionality in the socket. + * + * A broadcast is a UDP message to addresses like "192.168.255.255" or "255.255.255.255" that is + * delivered to every host on the network. + * The reason this is generally disabled by default is to prevent + * accidental broadcast messages from flooding the network. +**/ +- (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr; + +/** + * Asynchronously sends the given data, with the given timeout and tag. + * + * This method may only be used with a connected socket. + * + * If data is nil or zero-length, this method does nothing and immediately returns NO. + * If the socket is not connected, this method does nothing and immediately returns NO. +**/ +- (BOOL)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Asynchronously sends the given data, with the given timeout and tag, to the given host and port. + * + * This method cannot be used with a connected socket. + * + * If data is nil or zero-length, this method does nothing and immediately returns NO. + * If the socket is connected, this method does nothing and immediately returns NO. + * If unable to resolve host to a valid IPv4 or IPv6 address, this method returns NO. +**/ +- (BOOL)sendData:(NSData *)data toHost:(NSString *)host port:(UInt16)port withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Asynchronously sends the given data, with the given timeout and tag, to the given address. + * + * This method cannot be used with a connected socket. + * + * If data is nil or zero-length, this method does nothing and immediately returns NO. + * If the socket is connected, this method does nothing and immediately returns NO. +**/ +- (BOOL)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Asynchronously receives a single datagram packet. + * + * If the receive succeeds, the onUdpSocket:didReceiveData:fromHost:port:tag delegate method will be called. + * Otherwise, a timeout will occur, and the onUdpSocket:didNotReceiveDataWithTag: delegate method will be called. +**/ +- (void)receiveWithTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + * Closes the socket immediately. Any pending send or receive operations are dropped. +**/ +- (void)close; + +/** + * Closes after all pending send operations have completed. + * After calling this, the sendData: and receive: methods will do nothing. + * In other words, you won't be able to add any more send or receive operations to the queue. + * The socket will close even if there are still pending receive operations. +**/ +- (void)closeAfterSending; + +/** + * Closes after all pending receive operations have completed. + * After calling this, the sendData: and receive: methods will do nothing. + * In other words, you won't be able to add any more send or receive operations to the queue. + * The socket will close even if there are still pending send operations. +**/ +- (void)closeAfterReceiving; + +/** + * Closes after all pending send and receive operations have completed. + * After calling this, the sendData: and receive: methods will do nothing. + * In other words, you won't be able to add any more send or receive operations to the queue. +**/ +- (void)closeAfterSendingAndReceiving; + +/** + * Gets/Sets the maximum size of the buffer that will be allocated for receive operations. + * The default size is 9216 bytes. + * + * The theoretical maximum size of any IPv4 UDP packet is UINT16_MAX = 65535. + * The theoretical maximum size of any IPv6 UDP packet is UINT32_MAX = 4294967295. + * + * In practice, however, the size of UDP packets will be much smaller. + * Indeed most protocols will send and receive packets of only a few bytes, + * or will set a limit on the size of packets to prevent fragmentation in the IP layer. + * + * If you set the buffer size too small, the sockets API in the OS will silently discard + * any extra data, and you will not be notified of the error. +**/ +- (UInt32)maxReceiveBufferSize; +- (void)setMaxReceiveBufferSize:(UInt32)max; + +/** + * When you create an AsyncUdpSocket, it is added to the runloop of the current thread. + * So it is easiest to simply create the socket on the thread you intend to use it. + * + * If, however, you need to move the socket to a separate thread at a later time, this + * method may be used to accomplish the task. + * + * This method must be called from the thread/runloop the socket is currently running on. + * + * Note: After calling this method, all further method calls to this object should be done from the given runloop. + * Also, all delegate calls will be sent on the given runloop. +**/ +- (BOOL)moveToRunLoop:(NSRunLoop *)runLoop; + +/** + * Allows you to configure which run loop modes the socket uses. + * The default set of run loop modes is NSDefaultRunLoopMode. + * + * If you'd like your socket to continue operation during other modes, you may want to add modes such as + * NSModalPanelRunLoopMode or NSEventTrackingRunLoopMode. Or you may simply want to use NSRunLoopCommonModes. + * + * Note: NSRunLoopCommonModes is defined in 10.5. For previous versions one can use kCFRunLoopCommonModes. +**/ +- (BOOL)setRunLoopModes:(NSArray *)runLoopModes; + +/** + * Returns the current run loop modes the AsyncSocket instance is operating in. + * The default set of run loop modes is NSDefaultRunLoopMode. +**/ +- (NSArray *)runLoopModes; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@protocol AsyncUdpSocketDelegate +@optional + +/** + * Called when the datagram with the given tag has been sent. +**/ +- (void)onUdpSocket:(AsyncUdpSocket *)sock didSendDataWithTag:(long)tag; + +/** + * Called if an error occurs while trying to send a datagram. + * This could be due to a timeout, or something more serious such as the data being too large to fit in a sigle packet. +**/ +- (void)onUdpSocket:(AsyncUdpSocket *)sock didNotSendDataWithTag:(long)tag dueToError:(NSError *)error; + +/** + * Called when the socket has received the requested datagram. + * + * Due to the nature of UDP, you may occasionally receive undesired packets. + * These may be rogue UDP packets from unknown hosts, + * or they may be delayed packets arriving after retransmissions have already occurred. + * It's important these packets are properly ignored, while not interfering with the flow of your implementation. + * As an aid, this delegate method has a boolean return value. + * If you ever need to ignore a received packet, simply return NO, + * and AsyncUdpSocket will continue as if the packet never arrived. + * That is, the original receive request will still be queued, and will still timeout as usual if a timeout was set. + * For example, say you requested to receive data, and you set a timeout of 500 milliseconds, using a tag of 15. + * If rogue data arrives after 250 milliseconds, this delegate method would be invoked, and you could simply return NO. + * If the expected data then arrives within the next 250 milliseconds, + * this delegate method will be invoked, with a tag of 15, just as if the rogue data never appeared. + * + * Under normal circumstances, you simply return YES from this method. +**/ +- (BOOL)onUdpSocket:(AsyncUdpSocket *)sock + didReceiveData:(NSData *)data + withTag:(long)tag + fromHost:(NSString *)host + port:(UInt16)port; + +/** + * Called if an error occurs while trying to receive a requested datagram. + * This is generally due to a timeout, but could potentially be something else if some kind of OS error occurred. +**/ +- (void)onUdpSocket:(AsyncUdpSocket *)sock didNotReceiveDataWithTag:(long)tag dueToError:(NSError *)error; + +/** + * Called when the socket is closed. + * A socket is only closed if you explicitly call one of the close methods. +**/ +- (void)onUdpSocketDidClose:(AsyncUdpSocket *)sock; + +@end diff --git a/Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncUdpSocket.m b/Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncUdpSocket.m new file mode 100644 index 0000000..d4c19c3 --- /dev/null +++ b/Example/Pods/CocoaAsyncSocket/Source/RunLoop/AsyncUdpSocket.m @@ -0,0 +1,2313 @@ +// +// AsyncUdpSocket.m +// +// This class is in the public domain. +// Originally created by Robbie Hanson on Wed Oct 01 2008. +// Updated and maintained by Deusty Designs and the Mac development community. +// +// http://code.google.com/p/cocoaasyncsocket/ +// + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#import "AsyncUdpSocket.h" +#import +#import +#import +#import +#import +#import +#import + +#if TARGET_OS_IPHONE +// Note: You may need to add the CFNetwork Framework to your project +#import +#endif + + +#define SENDQUEUE_CAPACITY 5 // Initial capacity +#define RECEIVEQUEUE_CAPACITY 5 // Initial capacity + +#define DEFAULT_MAX_RECEIVE_BUFFER_SIZE 9216 + +NSString *const AsyncUdpSocketException = @"AsyncUdpSocketException"; +NSString *const AsyncUdpSocketErrorDomain = @"AsyncUdpSocketErrorDomain"; + +#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 +// Mutex lock used by all instances of AsyncUdpSocket, to protect getaddrinfo. +// Prior to Mac OS X 10.5 this method was not thread-safe. +static NSString *getaddrinfoLock = @"lock"; +#endif + +enum AsyncUdpSocketFlags +{ + kDidBind = 1 << 0, // If set, bind has been called. + kDidConnect = 1 << 1, // If set, connect has been called. + kSock4CanAcceptBytes = 1 << 2, // If set, we know socket4 can accept bytes. If unset, it's unknown. + kSock6CanAcceptBytes = 1 << 3, // If set, we know socket6 can accept bytes. If unset, it's unknown. + kSock4HasBytesAvailable = 1 << 4, // If set, we know socket4 has bytes available. If unset, it's unknown. + kSock6HasBytesAvailable = 1 << 5, // If set, we know socket6 has bytes available. If unset, it's unknown. + kForbidSendReceive = 1 << 6, // If set, no new send or receive operations are allowed to be queued. + kCloseAfterSends = 1 << 7, // If set, close as soon as no more sends are queued. + kCloseAfterReceives = 1 << 8, // If set, close as soon as no more receives are queued. + kDidClose = 1 << 9, // If set, the socket has been closed, and should not be used anymore. + kDequeueSendScheduled = 1 << 10, // If set, a maybeDequeueSend operation is already scheduled. + kDequeueReceiveScheduled = 1 << 11, // If set, a maybeDequeueReceive operation is already scheduled. + kFlipFlop = 1 << 12, // Used to alternate between IPv4 and IPv6 sockets. +}; + +@interface AsyncUdpSocket (Private) + +// Run Loop +- (void)runLoopAddSource:(CFRunLoopSourceRef)source; +- (void)runLoopRemoveSource:(CFRunLoopSourceRef)source; +- (void)runLoopAddTimer:(NSTimer *)timer; +- (void)runLoopRemoveTimer:(NSTimer *)timer; + +// Utilities +- (NSString *)addressHost4:(struct sockaddr_in *)pSockaddr4; +- (NSString *)addressHost6:(struct sockaddr_in6 *)pSockaddr6; +- (NSString *)addressHost:(struct sockaddr *)pSockaddr; + +// Disconnect Implementation +- (void)emptyQueues; +- (void)closeSocket4; +- (void)closeSocket6; +- (void)maybeScheduleClose; + +// Errors +- (NSError *)getErrnoError; +- (NSError *)getSocketError; +- (NSError *)getIPv4UnavailableError; +- (NSError *)getIPv6UnavailableError; +- (NSError *)getSendTimeoutError; +- (NSError *)getReceiveTimeoutError; + +// Diagnostics +- (NSString *)connectedHost:(CFSocketRef)socket; +- (UInt16)connectedPort:(CFSocketRef)socket; +- (NSString *)localHost:(CFSocketRef)socket; +- (UInt16)localPort:(CFSocketRef)socket; + +// Sending +- (BOOL)canAcceptBytes:(CFSocketRef)sockRef; +- (void)scheduleDequeueSend; +- (void)maybeDequeueSend; +- (void)doSend:(CFSocketRef)sockRef; +- (void)completeCurrentSend; +- (void)failCurrentSend:(NSError *)error; +- (void)endCurrentSend; +- (void)doSendTimeout:(NSTimer *)timer; + +// Receiving +- (BOOL)hasBytesAvailable:(CFSocketRef)sockRef; +- (void)scheduleDequeueReceive; +- (void)maybeDequeueReceive; +- (void)doReceive4; +- (void)doReceive6; +- (void)doReceive:(CFSocketRef)sockRef; +- (BOOL)maybeCompleteCurrentReceive; +- (void)failCurrentReceive:(NSError *)error; +- (void)endCurrentReceive; +- (void)doReceiveTimeout:(NSTimer *)timer; + +@end + +static void MyCFSocketCallback(CFSocketRef, CFSocketCallBackType, CFDataRef, const void *, void *); + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The AsyncSendPacket encompasses the instructions for a single send/write. +**/ +@interface AsyncSendPacket : NSObject +{ +@public + NSData *buffer; + NSData *address; + NSTimeInterval timeout; + long tag; +} +- (id)initWithData:(NSData *)d address:(NSData *)a timeout:(NSTimeInterval)t tag:(long)i; +@end + +@implementation AsyncSendPacket + +- (id)initWithData:(NSData *)d address:(NSData *)a timeout:(NSTimeInterval)t tag:(long)i +{ + if((self = [super init])) + { + buffer = d; + address = a; + timeout = t; + tag = i; + } + return self; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The AsyncReceivePacket encompasses the instructions for a single receive/read. +**/ +@interface AsyncReceivePacket : NSObject +{ +@public + NSTimeInterval timeout; + long tag; + NSData *buffer; + NSString *host; + UInt16 port; +} +- (id)initWithTimeout:(NSTimeInterval)t tag:(long)i; +@end + +@implementation AsyncReceivePacket + +- (id)initWithTimeout:(NSTimeInterval)t tag:(long)i +{ + if((self = [super init])) + { + timeout = t; + tag = i; + + buffer = nil; + host = nil; + port = 0; + } + return self; +} + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +@implementation AsyncUdpSocket + +- (id)initWithDelegate:(id)delegate userData:(long)userData enableIPv4:(BOOL)enableIPv4 enableIPv6:(BOOL)enableIPv6 +{ + if((self = [super init])) + { + theFlags = 0; + theDelegate = delegate; + theUserData = userData; + maxReceiveBufferSize = DEFAULT_MAX_RECEIVE_BUFFER_SIZE; + + theSendQueue = [[NSMutableArray alloc] initWithCapacity:SENDQUEUE_CAPACITY]; + theCurrentSend = nil; + theSendTimer = nil; + + theReceiveQueue = [[NSMutableArray alloc] initWithCapacity:RECEIVEQUEUE_CAPACITY]; + theCurrentReceive = nil; + theReceiveTimer = nil; + + // Socket context + theContext.version = 0; + theContext.info = (__bridge void *)self; + theContext.retain = nil; + theContext.release = nil; + theContext.copyDescription = nil; + + // Create the sockets + theSocket4 = NULL; + theSocket6 = NULL; + + if(enableIPv4) + { + theSocket4 = CFSocketCreate(kCFAllocatorDefault, + PF_INET, + SOCK_DGRAM, + IPPROTO_UDP, + kCFSocketReadCallBack | kCFSocketWriteCallBack, + (CFSocketCallBack)&MyCFSocketCallback, + &theContext); + } + if(enableIPv6) + { + theSocket6 = CFSocketCreate(kCFAllocatorDefault, + PF_INET6, + SOCK_DGRAM, + IPPROTO_UDP, + kCFSocketReadCallBack | kCFSocketWriteCallBack, + (CFSocketCallBack)&MyCFSocketCallback, + &theContext); + } + + // Disable continuous callbacks for read and write. + // If we don't do this, the socket(s) will just sit there firing read callbacks + // at us hundreds of times a second if we don't immediately read the available data. + if(theSocket4) + { + CFSocketSetSocketFlags(theSocket4, kCFSocketCloseOnInvalidate); + } + if(theSocket6) + { + CFSocketSetSocketFlags(theSocket6, kCFSocketCloseOnInvalidate); + } + + // Prevent sendto calls from sending SIGPIPE signal when socket has been shutdown for writing. + // sendto will instead let us handle errors as usual by returning -1. + int noSigPipe = 1; + if(theSocket4) + { + setsockopt(CFSocketGetNative(theSocket4), SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe)); + } + if(theSocket6) + { + setsockopt(CFSocketGetNative(theSocket6), SOL_SOCKET, SO_NOSIGPIPE, &noSigPipe, sizeof(noSigPipe)); + } + + // Get the CFRunLoop to which the socket should be attached. + theRunLoop = CFRunLoopGetCurrent(); + + // Set default run loop modes + theRunLoopModes = [NSArray arrayWithObject:NSDefaultRunLoopMode]; + + // Attach the sockets to the run loop + + if(theSocket4) + { + theSource4 = CFSocketCreateRunLoopSource(kCFAllocatorDefault, theSocket4, 0); + [self runLoopAddSource:theSource4]; + } + + if(theSocket6) + { + theSource6 = CFSocketCreateRunLoopSource(kCFAllocatorDefault, theSocket6, 0); + [self runLoopAddSource:theSource6]; + } + + cachedLocalPort = 0; + cachedConnectedPort = 0; + } + return self; +} + +- (id)init +{ + return [self initWithDelegate:nil userData:0 enableIPv4:YES enableIPv6:YES]; +} + +- (id)initWithDelegate:(id)delegate +{ + return [self initWithDelegate:delegate userData:0 enableIPv4:YES enableIPv6:YES]; +} + +- (id)initWithDelegate:(id)delegate userData:(long)userData +{ + return [self initWithDelegate:delegate userData:userData enableIPv4:YES enableIPv6:YES]; +} + +- (id)initIPv4 +{ + return [self initWithDelegate:nil userData:0 enableIPv4:YES enableIPv6:NO]; +} + +- (id)initIPv6 +{ + return [self initWithDelegate:nil userData:0 enableIPv4:NO enableIPv6:YES]; +} + +- (void) dealloc +{ + [self close]; + + [NSObject cancelPreviousPerformRequestsWithTarget:theDelegate selector:@selector(onUdpSocketDidClose:) object:self]; + [NSObject cancelPreviousPerformRequestsWithTarget:self]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Accessors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (id)delegate +{ + return theDelegate; +} + +- (void)setDelegate:(id)delegate +{ + theDelegate = delegate; +} + +- (long)userData +{ + return theUserData; +} + +- (void)setUserData:(long)userData +{ + theUserData = userData; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Run Loop +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)runLoopAddSource:(CFRunLoopSourceRef)source +{ + for (NSString *runLoopMode in theRunLoopModes) + { + CFRunLoopAddSource(theRunLoop, source, (__bridge CFStringRef)runLoopMode); + } +} + +- (void)runLoopRemoveSource:(CFRunLoopSourceRef)source +{ + for (NSString *runLoopMode in theRunLoopModes) + { + CFRunLoopRemoveSource(theRunLoop, source, (__bridge CFStringRef)runLoopMode); + } +} + +- (void)runLoopAddTimer:(NSTimer *)timer +{ + for (NSString *runLoopMode in theRunLoopModes) + { + CFRunLoopAddTimer(theRunLoop, (__bridge CFRunLoopTimerRef)timer, (__bridge CFStringRef)runLoopMode); + } +} + +- (void)runLoopRemoveTimer:(NSTimer *)timer +{ + for (NSString *runLoopMode in theRunLoopModes) + { + CFRunLoopRemoveTimer(theRunLoop, (__bridge CFRunLoopTimerRef)timer, (__bridge CFStringRef)runLoopMode); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (UInt32)maxReceiveBufferSize +{ + return maxReceiveBufferSize; +} + +- (void)setMaxReceiveBufferSize:(UInt32)max +{ + maxReceiveBufferSize = max; +} + +/** + * See the header file for a full explanation of this method. +**/ +- (BOOL)moveToRunLoop:(NSRunLoop *)runLoop +{ + NSAssert((theRunLoop == NULL) || (theRunLoop == CFRunLoopGetCurrent()), + @"moveToRunLoop must be called from within the current RunLoop!"); + + if(runLoop == nil) + { + return NO; + } + if(theRunLoop == [runLoop getCFRunLoop]) + { + return YES; + } + + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + theFlags &= ~kDequeueSendScheduled; + theFlags &= ~kDequeueReceiveScheduled; + + if(theSource4) [self runLoopRemoveSource:theSource4]; + if(theSource6) [self runLoopRemoveSource:theSource6]; + + if(theSendTimer) [self runLoopRemoveTimer:theSendTimer]; + if(theReceiveTimer) [self runLoopRemoveTimer:theReceiveTimer]; + + theRunLoop = [runLoop getCFRunLoop]; + + if(theSendTimer) [self runLoopAddTimer:theSendTimer]; + if(theReceiveTimer) [self runLoopAddTimer:theReceiveTimer]; + + if(theSource4) [self runLoopAddSource:theSource4]; + if(theSource6) [self runLoopAddSource:theSource6]; + + [runLoop performSelector:@selector(maybeDequeueSend) target:self argument:nil order:0 modes:theRunLoopModes]; + [runLoop performSelector:@selector(maybeDequeueReceive) target:self argument:nil order:0 modes:theRunLoopModes]; + [runLoop performSelector:@selector(maybeScheduleClose) target:self argument:nil order:0 modes:theRunLoopModes]; + + return YES; +} + +/** + * See the header file for a full explanation of this method. +**/ +- (BOOL)setRunLoopModes:(NSArray *)runLoopModes +{ + NSAssert((theRunLoop == NULL) || (theRunLoop == CFRunLoopGetCurrent()), + @"setRunLoopModes must be called from within the current RunLoop!"); + + if([runLoopModes count] == 0) + { + return NO; + } + if([theRunLoopModes isEqualToArray:runLoopModes]) + { + return YES; + } + + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + theFlags &= ~kDequeueSendScheduled; + theFlags &= ~kDequeueReceiveScheduled; + + if(theSource4) [self runLoopRemoveSource:theSource4]; + if(theSource6) [self runLoopRemoveSource:theSource6]; + + if(theSendTimer) [self runLoopRemoveTimer:theSendTimer]; + if(theReceiveTimer) [self runLoopRemoveTimer:theReceiveTimer]; + + theRunLoopModes = [runLoopModes copy]; + + if(theSendTimer) [self runLoopAddTimer:theSendTimer]; + if(theReceiveTimer) [self runLoopAddTimer:theReceiveTimer]; + + if(theSource4) [self runLoopAddSource:theSource4]; + if(theSource6) [self runLoopAddSource:theSource6]; + + [self performSelector:@selector(maybeDequeueSend) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + [self performSelector:@selector(maybeDequeueReceive) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + [self performSelector:@selector(maybeScheduleClose) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + + return YES; +} + +- (NSArray *)runLoopModes +{ + return [theRunLoopModes copy]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Utilities: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Attempts to convert the given host/port into and IPv4 and/or IPv6 data structure. + * The data structure is of type sockaddr_in for IPv4 and sockaddr_in6 for IPv6. + * + * Returns zero on success, or one of the error codes listed in gai_strerror if an error occurs (as per getaddrinfo). +**/ +- (int)convertForBindHost:(NSString *)host + port:(UInt16)port + intoAddress4:(NSData **)address4 + address6:(NSData **)address6 +{ + if(host == nil || ([host length] == 0)) + { + // Use ANY address + struct sockaddr_in nativeAddr; + nativeAddr.sin_len = sizeof(struct sockaddr_in); + nativeAddr.sin_family = AF_INET; + nativeAddr.sin_port = htons(port); + nativeAddr.sin_addr.s_addr = htonl(INADDR_ANY); + memset(&(nativeAddr.sin_zero), 0, sizeof(nativeAddr.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_any; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures for CFSocketSetAddress. + if(address4) *address4 = [NSData dataWithBytes:&nativeAddr length:sizeof(nativeAddr)]; + if(address6) *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + + return 0; + } + else if([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) + { + // Note: getaddrinfo("localhost",...) fails on 10.5.3 + + // Use LOOPBACK address + struct sockaddr_in nativeAddr; + nativeAddr.sin_len = sizeof(struct sockaddr_in); + nativeAddr.sin_family = AF_INET; + nativeAddr.sin_port = htons(port); + nativeAddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + memset(&(nativeAddr.sin_zero), 0, sizeof(nativeAddr.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_loopback; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures for CFSocketSetAddress. + if(address4) *address4 = [NSData dataWithBytes:&nativeAddr length:sizeof(nativeAddr)]; + if(address6) *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + + return 0; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + +#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 + @synchronized (getaddrinfoLock) +#endif + { + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + hints.ai_flags = AI_PASSIVE; + + int error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); + + if(error) return error; + + for(res = res0; res; res = res->ai_next) + { + if(address4 && !*address4 && (res->ai_family == AF_INET)) + { + // Found IPv4 address + // Wrap the native address structures for CFSocketSetAddress. + if(address4) *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + else if(address6 && !*address6 && (res->ai_family == AF_INET6)) + { + // Found IPv6 address + // Wrap the native address structures for CFSocketSetAddress. + if(address6) *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + } + freeaddrinfo(res0); + } + + return 0; + } +} + +/** + * Attempts to convert the given host/port into and IPv4 and/or IPv6 data structure. + * The data structure is of type sockaddr_in for IPv4 and sockaddr_in6 for IPv6. + * + * Returns zero on success, or one of the error codes listed in gai_strerror if an error occurs (as per getaddrinfo). +**/ +- (int)convertForSendHost:(NSString *)host + port:(UInt16)port + intoAddress4:(NSData **)address4 + address6:(NSData **)address6 +{ + if(host == nil || ([host length] == 0)) + { + // We're not binding, so what are we supposed to do with this? + return EAI_NONAME; + } + else if([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) + { + // Note: getaddrinfo("localhost",...) fails on 10.5.3 + + // Use LOOPBACK address + struct sockaddr_in nativeAddr; + nativeAddr.sin_len = sizeof(struct sockaddr_in); + nativeAddr.sin_family = AF_INET; + nativeAddr.sin_port = htons(port); + nativeAddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + memset(&(nativeAddr.sin_zero), 0, sizeof(nativeAddr.sin_zero)); + + struct sockaddr_in6 nativeAddr6; + nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); + nativeAddr6.sin6_family = AF_INET6; + nativeAddr6.sin6_port = htons(port); + nativeAddr6.sin6_flowinfo = 0; + nativeAddr6.sin6_addr = in6addr_loopback; + nativeAddr6.sin6_scope_id = 0; + + // Wrap the native address structures for CFSocketSetAddress. + if(address4) *address4 = [NSData dataWithBytes:&nativeAddr length:sizeof(nativeAddr)]; + if(address6) *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; + + return 0; + } + else + { + NSString *portStr = [NSString stringWithFormat:@"%hu", port]; + +#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 + @synchronized (getaddrinfoLock) +#endif + { + struct addrinfo hints, *res, *res0; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = PF_UNSPEC; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + // No passive flag on a send or connect + + int error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); + + if(error) return error; + + for(res = res0; res; res = res->ai_next) + { + if(address4 && !*address4 && (res->ai_family == AF_INET)) + { + // Found IPv4 address + // Wrap the native address structures for CFSocketSetAddress. + if(address4) *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + else if(address6 && !*address6 && (res->ai_family == AF_INET6)) + { + // Found IPv6 address + // Wrap the native address structures for CFSocketSetAddress. + if(address6) *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; + } + } + freeaddrinfo(res0); + } + + return 0; + } +} + +- (NSString *)addressHost4:(struct sockaddr_in *)pSockaddr4 +{ + char addrBuf[INET_ADDRSTRLEN]; + + if(inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, sizeof(addrBuf)) == NULL) + { + [NSException raise:NSInternalInconsistencyException format:@"Cannot convert address to string."]; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + +- (NSString *)addressHost6:(struct sockaddr_in6 *)pSockaddr6 +{ + char addrBuf[INET6_ADDRSTRLEN]; + + if(inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, sizeof(addrBuf)) == NULL) + { + [NSException raise:NSInternalInconsistencyException format:@"Cannot convert address to string."]; + } + + return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; +} + +- (NSString *)addressHost:(struct sockaddr *)pSockaddr +{ + if(pSockaddr->sa_family == AF_INET) + { + return [self addressHost4:(struct sockaddr_in *)pSockaddr]; + } + else + { + return [self addressHost6:(struct sockaddr_in6 *)pSockaddr]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Socket Implementation: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Binds the underlying socket(s) to the given port. + * The socket(s) will be able to receive data on any interface. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ +- (BOOL)bindToPort:(UInt16)port error:(NSError **)errPtr +{ + return [self bindToAddress:nil port:port error:errPtr]; +} + +/** + * Binds the underlying socket(s) to the given address and port. + * The sockets(s) will be able to receive data only on the given interface. + * + * To receive data on any interface, pass nil or "". + * To receive data only on the loopback interface, pass "localhost" or "loopback". + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ +- (BOOL)bindToAddress:(NSString *)host port:(UInt16)port error:(NSError **)errPtr +{ + if(theFlags & kDidClose) + { + [NSException raise:AsyncUdpSocketException + format:@"The socket is closed."]; + } + if(theFlags & kDidBind) + { + [NSException raise:AsyncUdpSocketException + format:@"Cannot bind a socket more than once."]; + } + if(theFlags & kDidConnect) + { + [NSException raise:AsyncUdpSocketException + format:@"Cannot bind after connecting. If needed, bind first, then connect."]; + } + + // Convert the given host/port into native address structures for CFSocketSetAddress + NSData *address4 = nil, *address6 = nil; + + int gai_error = [self convertForBindHost:host port:port intoAddress4:&address4 address6:&address6]; + if(gai_error) + { + if(errPtr) + { + NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + *errPtr = [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:info]; + } + return NO; + } + + NSAssert((address4 || address6), @"address4 and address6 are nil"); + + // Set the SO_REUSEADDR flags + + int reuseOn = 1; + if (theSocket4) setsockopt(CFSocketGetNative(theSocket4), SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + if (theSocket6) setsockopt(CFSocketGetNative(theSocket6), SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); + + // Bind the sockets + + if(address4) + { + if(theSocket4) + { + CFSocketError error = CFSocketSetAddress(theSocket4, (__bridge CFDataRef)address4); + if(error != kCFSocketSuccess) + { + if(errPtr) *errPtr = [self getSocketError]; + return NO; + } + + if(!address6) + { + // Using IPv4 only + [self closeSocket6]; + } + } + else if(!address6) + { + if(errPtr) *errPtr = [self getIPv4UnavailableError]; + return NO; + } + } + + if(address6) + { + // Note: The iPhone doesn't currently support IPv6 + + if(theSocket6) + { + CFSocketError error = CFSocketSetAddress(theSocket6, (__bridge CFDataRef)address6); + if(error != kCFSocketSuccess) + { + if(errPtr) *errPtr = [self getSocketError]; + return NO; + } + + if(!address4) + { + // Using IPv6 only + [self closeSocket4]; + } + } + else if(!address4) + { + if(errPtr) *errPtr = [self getIPv6UnavailableError]; + return NO; + } + } + + theFlags |= kDidBind; + return YES; +} + +/** + * Connects the underlying UDP socket to the given host and port. + * If an IPv4 address is resolved, the IPv4 socket is connected, and the IPv6 socket is invalidated and released. + * If an IPv6 address is resolved, the IPv6 socket is connected, and the IPv4 socket is invalidated and released. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ +- (BOOL)connectToHost:(NSString *)host onPort:(UInt16)port error:(NSError **)errPtr +{ + if(theFlags & kDidClose) + { + [NSException raise:AsyncUdpSocketException + format:@"The socket is closed."]; + } + if(theFlags & kDidConnect) + { + [NSException raise:AsyncUdpSocketException + format:@"Cannot connect a socket more than once."]; + } + + // Convert the given host/port into native address structures for CFSocketSetAddress + NSData *address4 = nil, *address6 = nil; + + int error = [self convertForSendHost:host port:port intoAddress4:&address4 address6:&address6]; + if(error) + { + if(errPtr) + { + NSString *errMsg = [NSString stringWithCString:gai_strerror(error) encoding:NSASCIIStringEncoding]; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + *errPtr = [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:error userInfo:info]; + } + return NO; + } + + NSAssert((address4 || address6), @"address4 and address6 are nil"); + + // We only want to connect via a single interface. + // IPv4 is currently preferred, but this may change in the future. + + CFSocketError sockErr; + + if (address4) + { + if (theSocket4) + { + sockErr = CFSocketConnectToAddress(theSocket4, (__bridge CFDataRef)address4, (CFTimeInterval)0.0); + if (sockErr != kCFSocketSuccess) + { + if(errPtr) *errPtr = [self getSocketError]; + return NO; + } + theFlags |= kDidConnect; + + // We're connected to an IPv4 address, so no need for the IPv6 socket + [self closeSocket6]; + + return YES; + } + else if(!address6) + { + if(errPtr) *errPtr = [self getIPv4UnavailableError]; + return NO; + } + } + + if (address6) + { + // Note: The iPhone doesn't currently support IPv6 + + if (theSocket6) + { + sockErr = CFSocketConnectToAddress(theSocket6, (__bridge CFDataRef)address6, (CFTimeInterval)0.0); + if (sockErr != kCFSocketSuccess) + { + if(errPtr) *errPtr = [self getSocketError]; + return NO; + } + theFlags |= kDidConnect; + + // We're connected to an IPv6 address, so no need for the IPv4 socket + [self closeSocket4]; + + return YES; + } + else + { + if(errPtr) *errPtr = [self getIPv6UnavailableError]; + return NO; + } + } + + // It shouldn't be possible to get to this point because either address4 or address6 was non-nil. + if(errPtr) *errPtr = nil; + return NO; +} + +/** + * Connects the underlying UDP socket to the remote address. + * If the address is an IPv4 address, the IPv4 socket is connected, and the IPv6 socket is invalidated and released. + * If the address is an IPv6 address, the IPv6 socket is connected, and the IPv4 socket is invalidated and released. + * + * The address is a native address structure, as may be returned from API's such as Bonjour. + * An address may be created manually by simply wrapping a sockaddr_in or sockaddr_in6 in an NSData object. + * + * On success, returns YES. + * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. +**/ +- (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr +{ + if (theFlags & kDidClose) + { + [NSException raise:AsyncUdpSocketException + format:@"The socket is closed."]; + } + if (theFlags & kDidConnect) + { + [NSException raise:AsyncUdpSocketException + format:@"Cannot connect a socket more than once."]; + } + + CFSocketError sockErr; + + // Is remoteAddr an IPv4 address? + if ([remoteAddr length] == sizeof(struct sockaddr_in)) + { + if (theSocket4) + { + sockErr = CFSocketConnectToAddress(theSocket4, (__bridge CFDataRef)remoteAddr, (CFTimeInterval)0.0); + if (sockErr != kCFSocketSuccess) + { + if(errPtr) *errPtr = [self getSocketError]; + return NO; + } + theFlags |= kDidConnect; + + // We're connected to an IPv4 address, so no need for the IPv6 socket + [self closeSocket6]; + + return YES; + } + else + { + if(errPtr) *errPtr = [self getIPv4UnavailableError]; + return NO; + } + } + + // Is remoteAddr an IPv6 address? + if ([remoteAddr length] == sizeof(struct sockaddr_in6)) + { + if (theSocket6) + { + sockErr = CFSocketConnectToAddress(theSocket6, (__bridge CFDataRef)remoteAddr, (CFTimeInterval)0.0); + if (sockErr != kCFSocketSuccess) + { + if(errPtr) *errPtr = [self getSocketError]; + return NO; + } + theFlags |= kDidConnect; + + // We're connected to an IPv6 address, so no need for the IPv4 socket + [self closeSocket4]; + + return YES; + } + else + { + if(errPtr) *errPtr = [self getIPv6UnavailableError]; + return NO; + } + } + + // The remoteAddr was invalid + if(errPtr) + { + NSString *errMsg = @"remoteAddr parameter is not a valid address"; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + *errPtr = [NSError errorWithDomain:AsyncUdpSocketErrorDomain + code:AsyncUdpSocketBadParameter + userInfo:info]; + } + return NO; +} + +/** + * Join multicast group + * + * Group should be a multicast IP address (eg. @"239.255.250.250" for IPv4). + * Address is local interface for IPv4, but currently defaults under IPv6. +**/ +- (BOOL)joinMulticastGroup:(NSString *)group error:(NSError **)errPtr +{ + return [self joinMulticastGroup:group withAddress:nil error:errPtr]; +} + +- (BOOL)joinMulticastGroup:(NSString *)group withAddress:(NSString *)address error:(NSError **)errPtr +{ + if(theFlags & kDidClose) + { + [NSException raise:AsyncUdpSocketException + format:@"The socket is closed."]; + } + if(!(theFlags & kDidBind)) + { + [NSException raise:AsyncUdpSocketException + format:@"Must bind a socket before joining a multicast group."]; + } + if(theFlags & kDidConnect) + { + [NSException raise:AsyncUdpSocketException + format:@"Cannot join a multicast group if connected."]; + } + + // Get local interface address + // Convert the given host/port into native address structures for CFSocketSetAddress + NSData *address4 = nil, *address6 = nil; + + int error = [self convertForBindHost:address port:0 intoAddress4:&address4 address6:&address6]; + if(error) + { + if(errPtr) + { + NSString *errMsg = [NSString stringWithCString:gai_strerror(error) encoding:NSASCIIStringEncoding]; + NSString *errDsc = [NSString stringWithFormat:@"Invalid parameter 'address': %@", errMsg]; + NSDictionary *info = [NSDictionary dictionaryWithObject:errDsc forKey:NSLocalizedDescriptionKey]; + + *errPtr = [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:error userInfo:info]; + } + return NO; + } + + NSAssert((address4 || address6), @"address4 and address6 are nil"); + + // Get multicast address (group) + NSData *group4 = nil, *group6 = nil; + + error = [self convertForBindHost:group port:0 intoAddress4:&group4 address6:&group6]; + if(error) + { + if(errPtr) + { + NSString *errMsg = [NSString stringWithCString:gai_strerror(error) encoding:NSASCIIStringEncoding]; + NSString *errDsc = [NSString stringWithFormat:@"Invalid parameter 'group': %@", errMsg]; + NSDictionary *info = [NSDictionary dictionaryWithObject:errDsc forKey:NSLocalizedDescriptionKey]; + + *errPtr = [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:error userInfo:info]; + } + return NO; + } + + NSAssert((group4 || group6), @"group4 and group6 are nil"); + + if(theSocket4 && group4 && address4) + { + const struct sockaddr_in* nativeAddress = [address4 bytes]; + const struct sockaddr_in* nativeGroup = [group4 bytes]; + + struct ip_mreq imreq; + imreq.imr_multiaddr = nativeGroup->sin_addr; + imreq.imr_interface = nativeAddress->sin_addr; + + // JOIN multicast group on default interface + error = setsockopt(CFSocketGetNative(theSocket4), IPPROTO_IP, IP_ADD_MEMBERSHIP, + (const void *)&imreq, sizeof(struct ip_mreq)); + if(error) + { + if(errPtr) + { + NSString *errMsg = @"Unable to join IPv4 multicast group"; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + *errPtr = [NSError errorWithDomain:@"kCFStreamErrorDomainPOSIX" code:error userInfo:info]; + } + return NO; + } + + // Using IPv4 only + [self closeSocket6]; + + return YES; + } + + if(theSocket6 && group6 && address6) + { + const struct sockaddr_in6* nativeGroup = [group6 bytes]; + + struct ipv6_mreq imreq; + imreq.ipv6mr_multiaddr = nativeGroup->sin6_addr; + imreq.ipv6mr_interface = 0; + + // JOIN multicast group on default interface + error = setsockopt(CFSocketGetNative(theSocket6), IPPROTO_IP, IPV6_JOIN_GROUP, + (const void *)&imreq, sizeof(struct ipv6_mreq)); + if(error) + { + if(errPtr) + { + NSString *errMsg = @"Unable to join IPv6 multicast group"; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + *errPtr = [NSError errorWithDomain:@"kCFStreamErrorDomainPOSIX" code:error userInfo:info]; + } + return NO; + } + + // Using IPv6 only + [self closeSocket4]; + + return YES; + } + + // The given address and group didn't match the existing socket(s). + // This means there were no compatible combination of all IPv4 or IPv6 socket, group and address. + if(errPtr) + { + NSString *errMsg = @"Invalid group and/or address, not matching existing socket(s)"; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + *errPtr = [NSError errorWithDomain:AsyncUdpSocketErrorDomain + code:AsyncUdpSocketBadParameter + userInfo:info]; + } + return NO; +} + +/** + * By default, the underlying socket in the OS will not allow you to send broadcast messages. + * In order to send broadcast messages, you need to enable this functionality in the socket. + * + * A broadcast is a UDP message to addresses like "192.168.255.255" or "255.255.255.255" that is + * delivered to every host on the network. + * The reason this is generally disabled by default is to prevent + * accidental broadcast messages from flooding the network. +**/ +- (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr +{ + if (theSocket4) + { + int value = flag ? 1 : 0; + int error = setsockopt(CFSocketGetNative(theSocket4), SOL_SOCKET, SO_BROADCAST, + (const void *)&value, sizeof(value)); + if(error) + { + if(errPtr) + { + NSString *errMsg = @"Unable to enable broadcast message sending"; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + *errPtr = [NSError errorWithDomain:@"kCFStreamErrorDomainPOSIX" code:error userInfo:info]; + } + return NO; + } + } + + // IPv6 does not implement broadcast, the ability to send a packet to all hosts on the attached link. + // The same effect can be achieved by sending a packet to the link-local all hosts multicast group. + + return YES; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Disconnect Implementation: +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)emptyQueues +{ + if (theCurrentSend) [self endCurrentSend]; + if (theCurrentReceive) [self endCurrentReceive]; + + [theSendQueue removeAllObjects]; + [theReceiveQueue removeAllObjects]; + + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(maybeDequeueSend) object:nil]; + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(maybeDequeueReceive) object:nil]; + + theFlags &= ~kDequeueSendScheduled; + theFlags &= ~kDequeueReceiveScheduled; +} + +- (void)closeSocket4 +{ + if (theSocket4 != NULL) + { + CFSocketInvalidate(theSocket4); + CFRelease(theSocket4); + theSocket4 = NULL; + } + if (theSource4 != NULL) + { + [self runLoopRemoveSource:theSource4]; + CFRelease(theSource4); + theSource4 = NULL; + } +} + +- (void)closeSocket6 +{ + if (theSocket6 != NULL) + { + CFSocketInvalidate(theSocket6); + CFRelease(theSocket6); + theSocket6 = NULL; + } + if (theSource6 != NULL) + { + [self runLoopRemoveSource:theSource6]; + CFRelease(theSource6); + theSource6 = NULL; + } +} + +- (void)close +{ + [self emptyQueues]; + [self closeSocket4]; + [self closeSocket6]; + + theRunLoop = NULL; + + // Delay notification to give user freedom to release without returning here and core-dumping. + if ([theDelegate respondsToSelector:@selector(onUdpSocketDidClose:)]) + { + [theDelegate performSelector:@selector(onUdpSocketDidClose:) + withObject:self + afterDelay:0 + inModes:theRunLoopModes]; + } + + theFlags |= kDidClose; +} + +- (void)closeAfterSending +{ + if(theFlags & kDidClose) return; + + theFlags |= (kForbidSendReceive | kCloseAfterSends); + [self maybeScheduleClose]; +} + +- (void)closeAfterReceiving +{ + if(theFlags & kDidClose) return; + + theFlags |= (kForbidSendReceive | kCloseAfterReceives); + [self maybeScheduleClose]; +} + +- (void)closeAfterSendingAndReceiving +{ + if(theFlags & kDidClose) return; + + theFlags |= (kForbidSendReceive | kCloseAfterSends | kCloseAfterReceives); + [self maybeScheduleClose]; +} + +- (void)maybeScheduleClose +{ + BOOL shouldDisconnect = NO; + + if(theFlags & kCloseAfterSends) + { + if(([theSendQueue count] == 0) && (theCurrentSend == nil)) + { + if(theFlags & kCloseAfterReceives) + { + if(([theReceiveQueue count] == 0) && (theCurrentReceive == nil)) + { + shouldDisconnect = YES; + } + } + else + { + shouldDisconnect = YES; + } + } + } + else if(theFlags & kCloseAfterReceives) + { + if(([theReceiveQueue count] == 0) && (theCurrentReceive == nil)) + { + shouldDisconnect = YES; + } + } + + if(shouldDisconnect) + { + [self performSelector:@selector(close) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Errors +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Returns a standard error object for the current errno value. + * Errno is used for low-level BSD socket errors. +**/ +- (NSError *)getErrnoError +{ + NSString *errorMsg = [NSString stringWithUTF8String:strerror(errno)]; + NSDictionary *userInfo = [NSDictionary dictionaryWithObject:errorMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; +} + +/** + * Returns a standard error message for a CFSocket error. + * Unfortunately, CFSocket offers no feedback on its errors. +**/ +- (NSError *)getSocketError +{ + NSString *errMsg = @"General CFSocket error"; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncUdpSocketErrorDomain code:AsyncUdpSocketCFSocketError userInfo:info]; +} + +- (NSError *)getIPv4UnavailableError +{ + NSString *errMsg = @"IPv4 is unavailable due to binding/connecting using IPv6 only"; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncUdpSocketErrorDomain code:AsyncUdpSocketIPv4Unavailable userInfo:info]; +} + +- (NSError *)getIPv6UnavailableError +{ + NSString *errMsg = @"IPv6 is unavailable due to binding/connecting using IPv4 only or is not supported on this platform"; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncUdpSocketErrorDomain code:AsyncUdpSocketIPv6Unavailable userInfo:info]; +} + +- (NSError *)getSendTimeoutError +{ + NSString *errMsg = @"Send operation timed out"; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncUdpSocketErrorDomain code:AsyncUdpSocketSendTimeoutError userInfo:info]; +} +- (NSError *)getReceiveTimeoutError +{ + NSString *errMsg = @"Receive operation timed out"; + NSDictionary *info = [NSDictionary dictionaryWithObject:errMsg forKey:NSLocalizedDescriptionKey]; + + return [NSError errorWithDomain:AsyncUdpSocketErrorDomain code:AsyncUdpSocketReceiveTimeoutError userInfo:info]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Diagnostics +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSString *)localHost +{ + if(cachedLocalHost) return cachedLocalHost; + + if(theSocket4) + return [self localHost:theSocket4]; + else + return [self localHost:theSocket6]; +} + +- (UInt16)localPort +{ + if(cachedLocalPort > 0) return cachedLocalPort; + + if(theSocket4) + return [self localPort:theSocket4]; + else + return [self localPort:theSocket6]; +} + +- (NSString *)connectedHost +{ + if(cachedConnectedHost) return cachedConnectedHost; + + if(theSocket4) + return [self connectedHost:theSocket4]; + else + return [self connectedHost:theSocket6]; +} + +- (UInt16)connectedPort +{ + if(cachedConnectedPort > 0) return cachedConnectedPort; + + if(theSocket4) + return [self connectedPort:theSocket4]; + else + return [self connectedPort:theSocket6]; +} + +- (NSString *)localHost:(CFSocketRef)theSocket +{ + if (theSocket == NULL) return nil; + + // Unfortunately we can't use CFSocketCopyAddress. + // The CFSocket library caches the address the first time you call CFSocketCopyAddress. + // So if this is called prior to binding/connecting/sending, it won't be updated again when necessary, + // and will continue to return the old value of the socket address. + + NSString *result = nil; + + if (theSocket == theSocket4) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(CFSocketGetNative(theSocket), (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + result = [self addressHost4:&sockaddr4]; + } + else + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(CFSocketGetNative(theSocket), (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + result = [self addressHost6:&sockaddr6]; + } + + if (theFlags & kDidBind) + { + cachedLocalHost = [result copy]; + } + + return result; +} + +- (UInt16)localPort:(CFSocketRef)theSocket +{ + if (theSocket == NULL) return 0; + + // Unfortunately we can't use CFSocketCopyAddress. + // The CFSocket library caches the address the first time you call CFSocketCopyAddress. + // So if this is called prior to binding/connecting/sending, it won't be updated again when necessary, + // and will continue to return the old value of the socket address. + + UInt16 result = 0; + + if (theSocket == theSocket4) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getsockname(CFSocketGetNative(theSocket), (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + result = ntohs(sockaddr4.sin_port); + } + else + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getsockname(CFSocketGetNative(theSocket), (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + result = ntohs(sockaddr6.sin6_port); + } + + if (theFlags & kDidBind) + { + cachedLocalPort = result; + } + + return result; +} + +- (NSString *)connectedHost:(CFSocketRef)theSocket +{ + if (theSocket == NULL) return nil; + + // Unfortunately we can't use CFSocketCopyPeerAddress. + // The CFSocket library caches the address the first time you call CFSocketCopyPeerAddress. + // So if this is called prior to binding/connecting/sending, it may not be updated again when necessary, + // and will continue to return the old value of the socket peer address. + + NSString *result = nil; + + if (theSocket == theSocket4) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(CFSocketGetNative(theSocket), (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return nil; + } + result = [self addressHost4:&sockaddr4]; + } + else + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(CFSocketGetNative(theSocket), (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return nil; + } + result = [self addressHost6:&sockaddr6]; + } + + if (theFlags & kDidConnect) + { + cachedConnectedHost = [result copy]; + } + + return result; +} + +- (UInt16)connectedPort:(CFSocketRef)theSocket +{ + if(theSocket == NULL) return 0; + + // Unfortunately we can't use CFSocketCopyPeerAddress. + // The CFSocket library caches the address the first time you call CFSocketCopyPeerAddress. + // So if this is called prior to binding/connecting/sending, it may not be updated again when necessary, + // and will continue to return the old value of the socket peer address. + + UInt16 result = 0; + + if(theSocket == theSocket4) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if(getpeername(CFSocketGetNative(theSocket), (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) + { + return 0; + } + result = ntohs(sockaddr4.sin_port); + } + else + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if(getpeername(CFSocketGetNative(theSocket), (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) + { + return 0; + } + result = ntohs(sockaddr6.sin6_port); + } + + if(theFlags & kDidConnect) + { + cachedConnectedPort = result; + } + + return result; +} + +- (BOOL)isConnected +{ + return (((theFlags & kDidConnect) != 0) && ((theFlags & kDidClose) == 0)); +} + +- (BOOL)isConnectedToHost:(NSString *)host port:(UInt16)port +{ + return [[self connectedHost] isEqualToString:host] && ([self connectedPort] == port); +} + +- (BOOL)isClosed +{ + return (theFlags & kDidClose) ? YES : NO; +} + +- (BOOL)isIPv4 +{ + return (theSocket4 != NULL); +} + +- (BOOL)isIPv6 +{ + return (theSocket6 != NULL); +} + +- (unsigned int)maximumTransmissionUnit +{ + CFSocketNativeHandle theNativeSocket; + if(theSocket4) + theNativeSocket = CFSocketGetNative(theSocket4); + else if(theSocket6) + theNativeSocket = CFSocketGetNative(theSocket6); + else + return 0; + + if(theNativeSocket == 0) + { + return 0; + } + + struct ifreq ifr; + bzero(&ifr, sizeof(ifr)); + + if(if_indextoname(theNativeSocket, ifr.ifr_name) == NULL) + { + return 0; + } + + if(ioctl(theNativeSocket, SIOCGIFMTU, &ifr) >= 0) + { + return ifr.ifr_mtu; + } + + return 0; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Sending +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + if([data length] == 0) return NO; + if(theFlags & kForbidSendReceive) return NO; + if(theFlags & kDidClose) return NO; + + // This method is only for connected sockets + if(![self isConnected]) return NO; + + AsyncSendPacket *packet = [[AsyncSendPacket alloc] initWithData:data address:nil timeout:timeout tag:tag]; + + [theSendQueue addObject:packet]; + [self scheduleDequeueSend]; + + return YES; +} + +- (BOOL)sendData:(NSData *)data + toHost:(NSString *)host + port:(UInt16)port + withTimeout:(NSTimeInterval)timeout + tag:(long)tag +{ + if([data length] == 0) return NO; + if(theFlags & kForbidSendReceive) return NO; + if(theFlags & kDidClose) return NO; + + // This method is only for non-connected sockets + if([self isConnected]) return NO; + + NSData *address4 = nil, *address6 = nil; + [self convertForSendHost:host port:port intoAddress4:&address4 address6:&address6]; + + AsyncSendPacket *packet = nil; + + if(address4 && theSocket4) + packet = [[AsyncSendPacket alloc] initWithData:data address:address4 timeout:timeout tag:tag]; + else if(address6 && theSocket6) + packet = [[AsyncSendPacket alloc] initWithData:data address:address6 timeout:timeout tag:tag]; + else + return NO; + + [theSendQueue addObject:packet]; + [self scheduleDequeueSend]; + + return YES; +} + +- (BOOL)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + if([data length] == 0) return NO; + if(theFlags & kForbidSendReceive) return NO; + if(theFlags & kDidClose) return NO; + + // This method is only for non-connected sockets + if([self isConnected]) return NO; + + if([remoteAddr length] == sizeof(struct sockaddr_in) && !theSocket4) + return NO; + + if([remoteAddr length] == sizeof(struct sockaddr_in6) && !theSocket6) + return NO; + + AsyncSendPacket *packet = [[AsyncSendPacket alloc] initWithData:data address:remoteAddr timeout:timeout tag:tag]; + + [theSendQueue addObject:packet]; + [self scheduleDequeueSend]; + + return YES; +} + +- (BOOL)canAcceptBytes:(CFSocketRef)sockRef +{ + if(sockRef == theSocket4) + { + if(theFlags & kSock4CanAcceptBytes) return YES; + } + else + { + if(theFlags & kSock6CanAcceptBytes) return YES; + } + + CFSocketNativeHandle theNativeSocket = CFSocketGetNative(sockRef); + + if(theNativeSocket == 0) + { + NSLog(@"Error - Could not get CFSocketNativeHandle from CFSocketRef"); + return NO; + } + + fd_set fds; + FD_ZERO(&fds); + FD_SET(theNativeSocket, &fds); + + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 0; + + return select(FD_SETSIZE, NULL, &fds, NULL, &timeout) > 0; +} + +- (CFSocketRef)socketForPacket:(AsyncSendPacket *)packet +{ + if(!theSocket4) + return theSocket6; + if(!theSocket6) + return theSocket4; + + return ([packet->address length] == sizeof(struct sockaddr_in)) ? theSocket4 : theSocket6; +} + +/** + * Puts a maybeDequeueSend on the run loop. +**/ +- (void)scheduleDequeueSend +{ + if((theFlags & kDequeueSendScheduled) == 0) + { + theFlags |= kDequeueSendScheduled; + [self performSelector:@selector(maybeDequeueSend) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + } +} + +/** + * This method starts a new send, if needed. + * It is called when a user requests a send. +**/ +- (void)maybeDequeueSend +{ + // Unset the flag indicating a call to this method is scheduled + theFlags &= ~kDequeueSendScheduled; + + if(theCurrentSend == nil) + { + if([theSendQueue count] > 0) + { + // Dequeue next send packet + theCurrentSend = [theSendQueue objectAtIndex:0]; + [theSendQueue removeObjectAtIndex:0]; + + // Start time-out timer. + if(theCurrentSend->timeout >= 0.0) + { + theSendTimer = [NSTimer timerWithTimeInterval:theCurrentSend->timeout + target:self + selector:@selector(doSendTimeout:) + userInfo:nil + repeats:NO]; + + [self runLoopAddTimer:theSendTimer]; + } + + // Immediately send, if possible. + [self doSend:[self socketForPacket:theCurrentSend]]; + } + else if(theFlags & kCloseAfterSends) + { + if(theFlags & kCloseAfterReceives) + { + if(([theReceiveQueue count] == 0) && (theCurrentReceive == nil)) + { + [self close]; + } + } + else + { + [self close]; + } + } + } +} + +/** + * This method is called when a new read is taken from the read queue or when new data becomes available on the stream. +**/ +- (void)doSend:(CFSocketRef)theSocket +{ + if(theCurrentSend != nil) + { + if(theSocket != [self socketForPacket:theCurrentSend]) + { + // Current send is for the other socket + return; + } + + if([self canAcceptBytes:theSocket]) + { + ssize_t result; + CFSocketNativeHandle theNativeSocket = CFSocketGetNative(theSocket); + + const void *buf = [theCurrentSend->buffer bytes]; + NSUInteger bufSize = [theCurrentSend->buffer length]; + + if([self isConnected]) + { + result = send(theNativeSocket, buf, (size_t)bufSize, 0); + } + else + { + const void *dst = [theCurrentSend->address bytes]; + NSUInteger dstSize = [theCurrentSend->address length]; + + result = sendto(theNativeSocket, buf, (size_t)bufSize, 0, dst, (socklen_t)dstSize); + } + + if(theSocket == theSocket4) + theFlags &= ~kSock4CanAcceptBytes; + else + theFlags &= ~kSock6CanAcceptBytes; + + if(result < 0) + { + [self failCurrentSend:[self getErrnoError]]; + } + else + { + // If it wasn't bound before, it's bound now + theFlags |= kDidBind; + + [self completeCurrentSend]; + } + + [self scheduleDequeueSend]; + } + else + { + // Request notification when the socket is ready to send more data + CFSocketEnableCallBacks(theSocket, kCFSocketReadCallBack | kCFSocketWriteCallBack); + } + } +} + +- (void)completeCurrentSend +{ + NSAssert (theCurrentSend, @"Trying to complete current send when there is no current send."); + + if ([theDelegate respondsToSelector:@selector(onUdpSocket:didSendDataWithTag:)]) + { + [theDelegate onUdpSocket:self didSendDataWithTag:theCurrentSend->tag]; + } + + if (theCurrentSend != nil) [self endCurrentSend]; // Caller may have disconnected. +} + +- (void)failCurrentSend:(NSError *)error +{ + NSAssert (theCurrentSend, @"Trying to fail current send when there is no current send."); + + if ([theDelegate respondsToSelector:@selector(onUdpSocket:didNotSendDataWithTag:dueToError:)]) + { + [theDelegate onUdpSocket:self didNotSendDataWithTag:theCurrentSend->tag dueToError:error]; + } + + if (theCurrentSend != nil) [self endCurrentSend]; // Caller may have disconnected. +} + +/** + * Ends the current send, and all associated variables such as the send timer. +**/ +- (void)endCurrentSend +{ + NSAssert (theCurrentSend, @"Trying to end current send when there is no current send."); + + [theSendTimer invalidate]; + theSendTimer = nil; + + theCurrentSend = nil; +} + +- (void)doSendTimeout:(NSTimer *)timer +{ + if (timer != theSendTimer) return; // Old timer. Ignore it. + if (theCurrentSend != nil) + { + [self failCurrentSend:[self getSendTimeoutError]]; + [self scheduleDequeueSend]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Receiving +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)receiveWithTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + if(theFlags & kForbidSendReceive) return; + if(theFlags & kDidClose) return; + + AsyncReceivePacket *packet = [[AsyncReceivePacket alloc] initWithTimeout:timeout tag:tag]; + + [theReceiveQueue addObject:packet]; + [self scheduleDequeueReceive]; +} + +- (BOOL)hasBytesAvailable:(CFSocketRef)sockRef +{ + if(sockRef == theSocket4) + { + if(theFlags & kSock4HasBytesAvailable) return YES; + } + else + { + if(theFlags & kSock6HasBytesAvailable) return YES; + } + + CFSocketNativeHandle theNativeSocket = CFSocketGetNative(sockRef); + + if(theNativeSocket == 0) + { + NSLog(@"Error - Could not get CFSocketNativeHandle from CFSocketRef"); + return NO; + } + + fd_set fds; + FD_ZERO(&fds); + FD_SET(theNativeSocket, &fds); + + struct timeval timeout; + timeout.tv_sec = 0; + timeout.tv_usec = 0; + + return select(FD_SETSIZE, &fds, NULL, NULL, &timeout) > 0; +} + +/** + * Puts a maybeDequeueReceive on the run loop. +**/ +- (void)scheduleDequeueReceive +{ + if((theFlags & kDequeueReceiveScheduled) == 0) + { + theFlags |= kDequeueReceiveScheduled; + [self performSelector:@selector(maybeDequeueReceive) withObject:nil afterDelay:0 inModes:theRunLoopModes]; + } +} + +/** + * Starts a new receive operation if needed +**/ +- (void)maybeDequeueReceive +{ + // Unset the flag indicating a call to this method is scheduled + theFlags &= ~kDequeueReceiveScheduled; + + if (theCurrentReceive == nil) + { + if ([theReceiveQueue count] > 0) + { + // Dequeue next receive packet + theCurrentReceive = [theReceiveQueue objectAtIndex:0]; + [theReceiveQueue removeObjectAtIndex:0]; + + // Start time-out timer. + if (theCurrentReceive->timeout >= 0.0) + { + theReceiveTimer = [NSTimer timerWithTimeInterval:theCurrentReceive->timeout + target:self + selector:@selector(doReceiveTimeout:) + userInfo:nil + repeats:NO]; + + [self runLoopAddTimer:theReceiveTimer]; + } + + // Immediately receive, if possible + // We always check both sockets so we don't ever starve one of them. + // We also check them in alternating orders to prevent starvation if both of them + // have a continuous flow of incoming data. + if(theFlags & kFlipFlop) + { + [self doReceive4]; + [self doReceive6]; + } + else + { + [self doReceive6]; + [self doReceive4]; + } + + theFlags ^= kFlipFlop; + } + else if(theFlags & kCloseAfterReceives) + { + if(theFlags & kCloseAfterSends) + { + if(([theSendQueue count] == 0) && (theCurrentSend == nil)) + { + [self close]; + } + } + else + { + [self close]; + } + } + } +} + +- (void)doReceive4 +{ + if(theSocket4) [self doReceive:theSocket4]; +} + +- (void)doReceive6 +{ + if(theSocket6) [self doReceive:theSocket6]; +} + +- (void)doReceive:(CFSocketRef)theSocket +{ + if (theCurrentReceive != nil) + { + BOOL appIgnoredReceivedData; + BOOL userIgnoredReceivedData; + + do + { + // Set or reset ignored variables. + // If the app or user ignores the received data, we'll continue this do-while loop. + appIgnoredReceivedData = NO; + userIgnoredReceivedData = NO; + + if([self hasBytesAvailable:theSocket]) + { + NSData* bufferData = nil; + ssize_t result; + CFSocketNativeHandle theNativeSocket = CFSocketGetNative(theSocket); + + // Allocate buffer for recvfrom operation. + // If the operation is successful, we'll realloc the buffer to the appropriate size, + // and create an NSData wrapper around it without needing to copy any bytes around. + void *buf = malloc(maxReceiveBufferSize); + size_t bufSize = maxReceiveBufferSize; + + if(theSocket == theSocket4) + { + struct sockaddr_in sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + result = recvfrom(theNativeSocket, buf, bufSize, 0, (struct sockaddr *)&sockaddr4, &sockaddr4len); + + if(result >= 0) + { + NSString *host = [self addressHost4:&sockaddr4]; + UInt16 port = ntohs(sockaddr4.sin_port); + + if([self isConnected] && ![self isConnectedToHost:host port:port]) + { + // The user connected to an address, and the received data doesn't match the address. + // This may happen if the data is received by the kernel prior to the connect call. + appIgnoredReceivedData = YES; + } + else + { + if(result != bufSize) + { + buf = realloc(buf, result); + } + bufferData = [[NSData alloc] initWithBytesNoCopy:buf + length:result + freeWhenDone:YES]; + theCurrentReceive->buffer = bufferData; + theCurrentReceive->host = host; + theCurrentReceive->port = port; + } + } + + theFlags &= ~kSock4HasBytesAvailable; + } + else + { + struct sockaddr_in6 sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + result = recvfrom(theNativeSocket, buf, bufSize, 0, (struct sockaddr *)&sockaddr6, &sockaddr6len); + + if(result >= 0) + { + NSString *host = [self addressHost6:&sockaddr6]; + UInt16 port = ntohs(sockaddr6.sin6_port); + + if([self isConnected] && ![self isConnectedToHost:host port:port]) + { + // The user connected to an address, and the received data doesn't match the address. + // This may happen if the data is received by the kernel prior to the connect call. + appIgnoredReceivedData = YES; + } + else + { + if(result != bufSize) + { + buf = realloc(buf, result); + } + bufferData = [[NSData alloc] initWithBytesNoCopy:buf + length:result + freeWhenDone:YES]; + theCurrentReceive->buffer = bufferData; + theCurrentReceive->host = host; + theCurrentReceive->port = port; + } + } + + theFlags &= ~kSock6HasBytesAvailable; + } + + // Check to see if we need to free our alloc'd buffer + // If bufferData is non-nil, it has taken ownership of the buffer + if(bufferData == nil) + { + free(buf); + } + + if(result < 0) + { + [self failCurrentReceive:[self getErrnoError]]; + [self scheduleDequeueReceive]; + } + else if(!appIgnoredReceivedData) + { + BOOL finished = [self maybeCompleteCurrentReceive]; + + if(finished) + { + [self scheduleDequeueReceive]; + } + else + { + theCurrentReceive->buffer = nil; + theCurrentReceive->host = nil; + + userIgnoredReceivedData = YES; + } + } + } + else + { + // Request notification when the socket is ready to receive more data + CFSocketEnableCallBacks(theSocket, kCFSocketReadCallBack | kCFSocketWriteCallBack); + } + + } while(appIgnoredReceivedData || userIgnoredReceivedData); + } +} + +- (BOOL)maybeCompleteCurrentReceive +{ + NSAssert (theCurrentReceive, @"Trying to complete current receive when there is no current receive."); + + BOOL finished = YES; + + if ([theDelegate respondsToSelector:@selector(onUdpSocket:didReceiveData:withTag:fromHost:port:)]) + { + finished = [theDelegate onUdpSocket:self + didReceiveData:theCurrentReceive->buffer + withTag:theCurrentReceive->tag + fromHost:theCurrentReceive->host + port:theCurrentReceive->port]; + } + + if (finished) + { + if (theCurrentReceive != nil) [self endCurrentReceive]; // Caller may have disconnected. + } + return finished; +} + +- (void)failCurrentReceive:(NSError *)error +{ + NSAssert (theCurrentReceive, @"Trying to fail current receive when there is no current receive."); + + if ([theDelegate respondsToSelector:@selector(onUdpSocket:didNotReceiveDataWithTag:dueToError:)]) + { + [theDelegate onUdpSocket:self didNotReceiveDataWithTag:theCurrentReceive->tag dueToError:error]; + } + + if (theCurrentReceive != nil) [self endCurrentReceive]; // Caller may have disconnected. +} + +- (void)endCurrentReceive +{ + NSAssert (theCurrentReceive, @"Trying to end current receive when there is no current receive."); + + [theReceiveTimer invalidate]; + theReceiveTimer = nil; + + theCurrentReceive = nil; +} + +- (void)doReceiveTimeout:(NSTimer *)timer +{ + if (timer != theReceiveTimer) return; // Old timer. Ignore it. + if (theCurrentReceive != nil) + { + [self failCurrentReceive:[self getReceiveTimeoutError]]; + [self scheduleDequeueReceive]; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark CF Callbacks +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)doCFSocketCallback:(CFSocketCallBackType)type + forSocket:(CFSocketRef)sock + withAddress:(NSData *)address + withData:(const void *)pData +{ + NSParameterAssert((sock == theSocket4) || (sock == theSocket6)); + + switch (type) + { + case kCFSocketReadCallBack: + if(sock == theSocket4) + theFlags |= kSock4HasBytesAvailable; + else + theFlags |= kSock6HasBytesAvailable; + [self doReceive:sock]; + break; + case kCFSocketWriteCallBack: + if(sock == theSocket4) + theFlags |= kSock4CanAcceptBytes; + else + theFlags |= kSock6CanAcceptBytes; + [self doSend:sock]; + break; + default: + NSLog (@"AsyncUdpSocket %p received unexpected CFSocketCallBackType %lu.", self, (unsigned long)type); + break; + } +} + +/** + * This is the callback we setup for CFSocket. + * This method does nothing but forward the call to it's Objective-C counterpart +**/ +static void MyCFSocketCallback(CFSocketRef sref, CFSocketCallBackType type, CFDataRef address, const void *pData, void *pInfo) +{ + @autoreleasepool { + + AsyncUdpSocket *theSocket = (__bridge AsyncUdpSocket *)pInfo; + [theSocket doCFSocketCallback:type forSocket:sref withAddress:(__bridge NSData *)address withData:pData]; + + } +} + +@end diff --git a/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.h b/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.h new file mode 100644 index 0000000..0b568fb --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.h @@ -0,0 +1,81 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2015, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software 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. +// +// * Neither the name of Deusty nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific +// prior written permission of Deusty, LLC. + +/** + * Welcome to CocoaLumberjack! + * + * The project page has a wealth of documentation if you have any questions. + * https://github.com/CocoaLumberjack/CocoaLumberjack + * + * If you're new to the project you may wish to read "Getting Started" at: + * Documentation/GettingStarted.md + * + * Otherwise, here is a quick refresher. + * There are three steps to using the macros: + * + * Step 1: + * Import the header in your implementation or prefix file: + * + * #import + * + * Step 2: + * Define your logging level in your implementation file: + * + * // Log levels: off, error, warn, info, verbose + * static const DDLogLevel ddLogLevel = DDLogLevelVerbose; + * + * Step 2 [3rd party frameworks]: + * + * Define your LOG_LEVEL_DEF to a different variable/function than ddLogLevel: + * + * // #undef LOG_LEVEL_DEF // Undefine first only if needed + * #define LOG_LEVEL_DEF myLibLogLevel + * + * Define your logging level in your implementation file: + * + * // Log levels: off, error, warn, info, verbose + * static const DDLogLevel myLibLogLevel = DDLogLevelVerbose; + * + * Step 3: + * Replace your NSLog statements with DDLog statements according to the severity of the message. + * + * NSLog(@"Fatal error, no dohickey found!"); -> DDLogError(@"Fatal error, no dohickey found!"); + * + * DDLog works exactly the same as NSLog. + * This means you can pass it multiple variables just like NSLog. + **/ + +#import + +// Disable legacy macros +#ifndef DD_LEGACY_MACROS + #define DD_LEGACY_MACROS 0 +#endif + +// Core +#import "DDLog.h" + +// Main macros +#import "DDLogMacros.h" +#import "DDAssertMacros.h" + +// Capture ASL +#import "DDASLLogCapture.h" + +// Loggers +#import "DDTTYLogger.h" +#import "DDASLLogger.h" +#import "DDFileLogger.h" + diff --git a/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.swift b/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.swift new file mode 100644 index 0000000..5f022ce --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Classes/CocoaLumberjack.swift @@ -0,0 +1,91 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2014-2015, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software 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. +// +// * Neither the name of Deusty nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific +// prior written permission of Deusty, LLC. + +import Foundation + +extension DDLogFlag { + public static func fromLogLevel(logLevel: DDLogLevel) -> DDLogFlag { + return DDLogFlag(rawValue: logLevel.rawValue) + } + + public init(_ logLevel: DDLogLevel) { + self = DDLogFlag(rawValue: logLevel.rawValue) + } + + ///returns the log level, or the lowest equivalant. + public func toLogLevel() -> DDLogLevel { + if let ourValid = DDLogLevel(rawValue: self.rawValue) { + return ourValid + } else { + let logFlag:DDLogFlag = self + + if logFlag.contains(.Verbose) { + return .Verbose + } else if logFlag.contains(.Debug) { + return .Debug + } else if logFlag.contains(.Info) { + return .Info + } else if logFlag.contains(.Warning) { + return .Warning + } else if logFlag.contains(.Error) { + return .Error + } else { + return .Off + } + } + } +} + +public var defaultDebugLevel = DDLogLevel.Verbose + +public func resetDefaultDebugLevel() { + defaultDebugLevel = DDLogLevel.Verbose +} + +public func SwiftLogMacro(isAsynchronous: Bool, level: DDLogLevel, flag flg: DDLogFlag, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, @autoclosure string: () -> String) { + if level.rawValue & flg.rawValue != 0 { + // Tell the DDLogMessage constructor to copy the C strings that get passed to it. + // Using string interpolation to prevent integer overflow warning when using StaticString.stringValue + let logMessage = DDLogMessage(message: string(), level: level, flag: flg, context: context, file: "\(file)", function: "\(function)", line: line, tag: tag, options: [.CopyFile, .CopyFunction], timestamp: nil) + DDLog.log(isAsynchronous, message: logMessage) + } +} + +public func DDLogDebug(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = true) { + SwiftLogMacro(async, level: level, flag: .Debug, context: context, file: file, function: function, line: line, tag: tag, string: logText) +} + +public func DDLogInfo(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = true) { + SwiftLogMacro(async, level: level, flag: .Info, context: context, file: file, function: function, line: line, tag: tag, string: logText) +} + +public func DDLogWarn(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = true) { + SwiftLogMacro(async, level: level, flag: .Warning, context: context, file: file, function: function, line: line, tag: tag, string: logText) +} + +public func DDLogVerbose(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = true) { + SwiftLogMacro(async, level: level, flag: .Verbose, context: context, file: file, function: function, line: line, tag: tag, string: logText) +} + +public func DDLogError(@autoclosure logText: () -> String, level: DDLogLevel = defaultDebugLevel, context: Int = 0, file: StaticString = __FILE__, function: StaticString = __FUNCTION__, line: UInt = __LINE__, tag: AnyObject? = nil, asynchronous async: Bool = false) { + SwiftLogMacro(async, level: level, flag: .Error, context: context, file: file, function: function, line: line, tag: tag, string: logText) +} + +/// Analogous to the C preprocessor macro `THIS_FILE`. +public func CurrentFileName(fileName: StaticString = __FILE__) -> String { + // Using string interpolation to prevent integer overflow warning when using StaticString.stringValue + // This double-casting to NSString is necessary as changes to how Swift handles NSPathUtilities requres the string to be an NSString + return (("\(fileName)" as NSString).lastPathComponent as NSString).stringByDeletingPathExtension +} diff --git a/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.h b/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.h new file mode 100644 index 0000000..f7fa79f --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.h @@ -0,0 +1,48 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2015, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software 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. +// +// * Neither the name of Deusty nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific +// prior written permission of Deusty, LLC. + +#import "DDASLLogger.h" + +@protocol DDLogger; + +/** + * This class provides the ability to capture the ASL (Apple System Logs) + */ +@interface DDASLLogCapture : NSObject + +/** + * Start capturing logs + */ ++ (void)start; + +/** + * Stop capturing logs + */ ++ (void)stop; + +/** + * Returns the current capture level. + * @note Default log level: DDLogLevelVerbose (i.e. capture all ASL messages). + */ ++ (DDLogLevel)captureLevel; + +/** + * Set the capture level + * + * @param level new level + */ ++ (void)setCaptureLevel:(DDLogLevel)level; + +@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.m b/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.m new file mode 100644 index 0000000..98d5342 --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Classes/DDASLLogCapture.m @@ -0,0 +1,230 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2015, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software 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. +// +// * Neither the name of Deusty nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific +// prior written permission of Deusty, LLC. + +#import "DDASLLogCapture.h" + +// Disable legacy macros +#ifndef DD_LEGACY_MACROS + #define DD_LEGACY_MACROS 0 +#endif + +#import "DDLog.h" + +#include +#include +#include +#include + +static BOOL _cancel = YES; +static DDLogLevel _captureLevel = DDLogLevelVerbose; + +#ifdef __IPHONE_8_0 + #define DDASL_IOS_PIVOT_VERSION __IPHONE_8_0 +#endif +#ifdef __MAC_10_10 + #define DDASL_OSX_PIVOT_VERSION __MAC_10_10 +#endif + +@implementation DDASLLogCapture + +static aslmsg (*dd_asl_next)(aslresponse obj); +static void (*dd_asl_release)(aslresponse obj); + ++ (void)initialize +{ + #if (defined(DDASL_IOS_PIVOT_VERSION) && __IPHONE_OS_VERSION_MAX_ALLOWED >= DDASL_IOS_PIVOT_VERSION) || (defined(DDASL_OSX_PIVOT_VERSION) && __MAC_OS_X_VERSION_MAX_ALLOWED >= DDASL_OSX_PIVOT_VERSION) + #if __IPHONE_OS_VERSION_MIN_REQUIRED < DDASL_IOS_PIVOT_VERSION || __MAC_OS_X_VERSION_MIN_REQUIRED < DDASL_OSX_PIVOT_VERSION + #pragma GCC diagnostic push + #pragma GCC diagnostic ignored "-Wdeprecated-declarations" + // Building on falsely advertised SDK, targeting deprecated API + dd_asl_next = &aslresponse_next; + dd_asl_release = &aslresponse_free; + #pragma GCC diagnostic pop + #else + // Building on lastest, correct SDK, targeting latest API + dd_asl_next = &asl_next; + dd_asl_release = &asl_release; + #endif + #else + // Building on old SDKs, targeting deprecated API + dd_asl_next = &aslresponse_next; + dd_asl_release = &aslresponse_free; + #endif +} + ++ (void)start { + // Ignore subsequent calls + if (!_cancel) { + return; + } + + _cancel = NO; + + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void) { + [self captureAslLogs]; + }); +} + ++ (void)stop { + _cancel = YES; +} + ++ (DDLogLevel)captureLevel { + return _captureLevel; +} + ++ (void)setCaptureLevel:(DDLogLevel)level { + _captureLevel = level; +} + +#pragma mark - Private methods + ++ (void)configureAslQuery:(aslmsg)query { + const char param[] = "7"; // ASL_LEVEL_DEBUG, which is everything. We'll rely on regular DDlog log level to filter + + asl_set_query(query, ASL_KEY_LEVEL, param, ASL_QUERY_OP_LESS_EQUAL | ASL_QUERY_OP_NUMERIC); + + // Don't retrieve logs from our own DDASLLogger + asl_set_query(query, kDDASLKeyDDLog, kDDASLDDLogValue, ASL_QUERY_OP_NOT_EQUAL); + +#if !TARGET_OS_IPHONE || TARGET_SIMULATOR + int processId = [[NSProcessInfo processInfo] processIdentifier]; + char pid[16]; + sprintf(pid, "%d", processId); + asl_set_query(query, ASL_KEY_PID, pid, ASL_QUERY_OP_EQUAL | ASL_QUERY_OP_NUMERIC); +#endif +} + ++ (void)aslMessageReceived:(aslmsg)msg { + const char* messageCString = asl_get( msg, ASL_KEY_MSG ); + if ( messageCString == NULL ) + return; + + int flag; + BOOL async; + + const char* levelCString = asl_get(msg, ASL_KEY_LEVEL); + switch (levelCString? atoi(levelCString) : 0) { + // By default all NSLog's with a ASL_LEVEL_WARNING level + case ASL_LEVEL_EMERG : + case ASL_LEVEL_ALERT : + case ASL_LEVEL_CRIT : flag = DDLogFlagError; async = NO; break; + case ASL_LEVEL_ERR : flag = DDLogFlagWarning; async = YES; break; + case ASL_LEVEL_WARNING : flag = DDLogFlagInfo; async = YES; break; + case ASL_LEVEL_NOTICE : flag = DDLogFlagDebug; async = YES; break; + case ASL_LEVEL_INFO : + case ASL_LEVEL_DEBUG : + default : flag = DDLogFlagVerbose; async = YES; break; + } + + if (!(_captureLevel & flag)) { + return; + } + + // NSString * sender = [NSString stringWithCString:asl_get(msg, ASL_KEY_SENDER) encoding:NSUTF8StringEncoding]; + NSString *message = @(messageCString); + + const char* secondsCString = asl_get( msg, ASL_KEY_TIME ); + const char* nanoCString = asl_get( msg, ASL_KEY_TIME_NSEC ); + NSTimeInterval seconds = secondsCString ? strtod(secondsCString, NULL) : [NSDate timeIntervalSinceReferenceDate] - NSTimeIntervalSince1970; + double nanoSeconds = nanoCString? strtod(nanoCString, NULL) : 0; + NSTimeInterval totalSeconds = seconds + (nanoSeconds / 1e9); + + NSDate *timeStamp = [NSDate dateWithTimeIntervalSince1970:totalSeconds]; + + DDLogMessage *logMessage = [[DDLogMessage alloc]initWithMessage:message + level:_captureLevel + flag:flag + context:0 + file:@"DDASLLogCapture" + function:0 + line:0 + tag:nil + options:0 + timestamp:timeStamp]; + + [DDLog log:async message:logMessage]; +} + ++ (void)captureAslLogs { + @autoreleasepool + { + /* + We use ASL_KEY_MSG_ID to see each message once, but there's no + obvious way to get the "next" ID. To bootstrap the process, we'll + search by timestamp until we've seen a message. + */ + + struct timeval timeval = { + .tv_sec = 0 + }; + gettimeofday(&timeval, NULL); + unsigned long long startTime = timeval.tv_sec; + __block unsigned long long lastSeenID = 0; + + /* + syslogd posts kNotifyASLDBUpdate (com.apple.system.logger.message) + through the notify API when it saves messages to the ASL database. + There is some coalescing - currently it is sent at most twice per + second - but there is no documented guarantee about this. In any + case, there may be multiple messages per notification. + + Notify notifications don't carry any payload, so we need to search + for the messages. + */ + int notifyToken = 0; // Can be used to unregister with notify_cancel(). + notify_register_dispatch(kNotifyASLDBUpdate, ¬ifyToken, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(int token) + { + // At least one message has been posted; build a search query. + @autoreleasepool + { + aslmsg query = asl_new(ASL_TYPE_QUERY); + char stringValue[64]; + + if (lastSeenID > 0) { + snprintf(stringValue, sizeof stringValue, "%llu", lastSeenID); + asl_set_query(query, ASL_KEY_MSG_ID, stringValue, ASL_QUERY_OP_GREATER | ASL_QUERY_OP_NUMERIC); + } else { + snprintf(stringValue, sizeof stringValue, "%llu", startTime); + asl_set_query(query, ASL_KEY_TIME, stringValue, ASL_QUERY_OP_GREATER_EQUAL | ASL_QUERY_OP_NUMERIC); + } + + [self configureAslQuery:query]; + + // Iterate over new messages. + aslmsg msg; + aslresponse response = asl_search(NULL, query); + + while ((msg = dd_asl_next(response))) + { + [self aslMessageReceived:msg]; + + // Keep track of which messages we've seen. + lastSeenID = atoll(asl_get(msg, ASL_KEY_MSG_ID)); + } + dd_asl_release(response); + asl_free(query); + + if (_cancel) { + notify_cancel(token); + return; + } + + } + }); + } +} + +@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.h b/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.h new file mode 100644 index 0000000..24cc1c3 --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.h @@ -0,0 +1,58 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2015, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software 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. +// +// * Neither the name of Deusty nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific +// prior written permission of Deusty, LLC. + +#import + +// Disable legacy macros +#ifndef DD_LEGACY_MACROS + #define DD_LEGACY_MACROS 0 +#endif + +#import "DDLog.h" + +// Custom key set on messages sent to ASL +extern const char* const kDDASLKeyDDLog; + +// Value set for kDDASLKeyDDLog +extern const char* const kDDASLDDLogValue; + +/** + * This class provides a logger for the Apple System Log facility. + * + * As described in the "Getting Started" page, + * the traditional NSLog() function directs its output to two places: + * + * - Apple System Log + * - StdErr (if stderr is a TTY) so log statements show up in Xcode console + * + * To duplicate NSLog() functionality you can simply add this logger and a tty logger. + * However, if you instead choose to use file logging (for faster performance), + * you may choose to use a file logger and a tty logger. + **/ +@interface DDASLLogger : DDAbstractLogger + +/** + * Singleton method + * + * @return the shared instance + */ ++ (instancetype)sharedInstance; + +// Inherited from DDAbstractLogger + +// - (id )logFormatter; +// - (void)setLogFormatter:(id )formatter; + +@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.m b/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.m new file mode 100644 index 0000000..90061c8 --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Classes/DDASLLogger.m @@ -0,0 +1,121 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2015, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software 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. +// +// * Neither the name of Deusty nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific +// prior written permission of Deusty, LLC. + +#import "DDASLLogger.h" +#import + +#if !__has_feature(objc_arc) +#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +const char* const kDDASLKeyDDLog = "DDLog"; + +const char* const kDDASLDDLogValue = "1"; + +static DDASLLogger *sharedInstance; + +@interface DDASLLogger () { + aslclient _client; +} + +@end + + +@implementation DDASLLogger + ++ (instancetype)sharedInstance { + static dispatch_once_t DDASLLoggerOnceToken; + + dispatch_once(&DDASLLoggerOnceToken, ^{ + sharedInstance = [[[self class] alloc] init]; + }); + + return sharedInstance; +} + +- (instancetype)init { + if (sharedInstance != nil) { + return nil; + } + + if ((self = [super init])) { + // A default asl client is provided for the main thread, + // but background threads need to create their own client. + + _client = asl_open(NULL, "com.apple.console", 0); + } + + return self; +} + +- (void)logMessage:(DDLogMessage *)logMessage { + // Skip captured log messages + if ([logMessage->_fileName isEqualToString:@"DDASLLogCapture"]) { + return; + } + + NSString * message = _logFormatter ? [_logFormatter formatLogMessage:logMessage] : logMessage->_message; + + if (logMessage) { + const char *msg = [message UTF8String]; + + size_t aslLogLevel; + switch (logMessage->_flag) { + // Note: By default ASL will filter anything above level 5 (Notice). + // So our mappings shouldn't go above that level. + case DDLogFlagError : aslLogLevel = ASL_LEVEL_CRIT; break; + case DDLogFlagWarning : aslLogLevel = ASL_LEVEL_ERR; break; + case DDLogFlagInfo : aslLogLevel = ASL_LEVEL_WARNING; break; // Regular NSLog's level + case DDLogFlagDebug : + case DDLogFlagVerbose : + default : aslLogLevel = ASL_LEVEL_NOTICE; break; + } + + static char const *const level_strings[] = { "0", "1", "2", "3", "4", "5", "6", "7" }; + + // NSLog uses the current euid to set the ASL_KEY_READ_UID. + uid_t const readUID = geteuid(); + + char readUIDString[16]; +#ifndef NS_BLOCK_ASSERTIONS + int l = snprintf(readUIDString, sizeof(readUIDString), "%d", readUID); +#else + snprintf(readUIDString, sizeof(readUIDString), "%d", readUID); +#endif + + NSAssert(l < sizeof(readUIDString), + @"Formatted euid is too long."); + NSAssert(aslLogLevel < (sizeof(level_strings) / sizeof(level_strings[0])), + @"Unhandled ASL log level."); + + aslmsg m = asl_new(ASL_TYPE_MSG); + if (m != NULL) { + if (asl_set(m, ASL_KEY_LEVEL, level_strings[aslLogLevel]) == 0 && + asl_set(m, ASL_KEY_MSG, msg) == 0 && + asl_set(m, ASL_KEY_READ_UID, readUIDString) == 0 && + asl_set(m, kDDASLKeyDDLog, kDDASLDDLogValue) == 0) { + asl_send(_client, m); + } + asl_free(m); + } + //TODO handle asl_* failures non-silently? + } +} + +- (NSString *)loggerName { + return @"cocoa.lumberjack.aslLogger"; +} + +@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.h b/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.h new file mode 100644 index 0000000..aad3666 --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.h @@ -0,0 +1,123 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2015, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software 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. +// +// * Neither the name of Deusty nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific +// prior written permission of Deusty, LLC. + +// Disable legacy macros +#ifndef DD_LEGACY_MACROS + #define DD_LEGACY_MACROS 0 +#endif + +#import "DDLog.h" + +/** + * This class provides an abstract implementation of a database logger. + * + * That is, it provides the base implementation for a database logger to build atop of. + * All that is needed for a concrete database logger is to extend this class + * and override the methods in the implementation file that are prefixed with "db_". + **/ +@interface DDAbstractDatabaseLogger : DDAbstractLogger { + +@protected + NSUInteger _saveThreshold; + NSTimeInterval _saveInterval; + NSTimeInterval _maxAge; + NSTimeInterval _deleteInterval; + BOOL _deleteOnEverySave; + + BOOL _saveTimerSuspended; + NSUInteger _unsavedCount; + dispatch_time_t _unsavedTime; + dispatch_source_t _saveTimer; + dispatch_time_t _lastDeleteTime; + dispatch_source_t _deleteTimer; +} + +/** + * Specifies how often to save the data to disk. + * Since saving is an expensive operation (disk io) it is not done after every log statement. + * These properties allow you to configure how/when the logger saves to disk. + * + * A save is done when either (whichever happens first): + * + * - The number of unsaved log entries reaches saveThreshold + * - The amount of time since the oldest unsaved log entry was created reaches saveInterval + * + * You can optionally disable the saveThreshold by setting it to zero. + * If you disable the saveThreshold you are entirely dependent on the saveInterval. + * + * You can optionally disable the saveInterval by setting it to zero (or a negative value). + * If you disable the saveInterval you are entirely dependent on the saveThreshold. + * + * It's not wise to disable both saveThreshold and saveInterval. + * + * The default saveThreshold is 500. + * The default saveInterval is 60 seconds. + **/ +@property (assign, readwrite) NSUInteger saveThreshold; + +/** + * See the description for the `saveThreshold` property + */ +@property (assign, readwrite) NSTimeInterval saveInterval; + +/** + * It is likely you don't want the log entries to persist forever. + * Doing so would allow the database to grow infinitely large over time. + * + * The maxAge property provides a way to specify how old a log statement can get + * before it should get deleted from the database. + * + * The deleteInterval specifies how often to sweep for old log entries. + * Since deleting is an expensive operation (disk io) is is done on a fixed interval. + * + * An alternative to the deleteInterval is the deleteOnEverySave option. + * This specifies that old log entries should be deleted during every save operation. + * + * You can optionally disable the maxAge by setting it to zero (or a negative value). + * If you disable the maxAge then old log statements are not deleted. + * + * You can optionally disable the deleteInterval by setting it to zero (or a negative value). + * + * If you disable both deleteInterval and deleteOnEverySave then old log statements are not deleted. + * + * It's not wise to enable both deleteInterval and deleteOnEverySave. + * + * The default maxAge is 7 days. + * The default deleteInterval is 5 minutes. + * The default deleteOnEverySave is NO. + **/ +@property (assign, readwrite) NSTimeInterval maxAge; + +/** + * See the description for the `maxAge` property + */ +@property (assign, readwrite) NSTimeInterval deleteInterval; + +/** + * See the description for the `maxAge` property + */ +@property (assign, readwrite) BOOL deleteOnEverySave; + +/** + * Forces a save of any pending log entries (flushes log entries to disk). + **/ +- (void)savePendingLogEntries; + +/** + * Removes any log entries that are older than maxAge. + **/ +- (void)deleteOldLogEntries; + +@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.m b/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.m new file mode 100644 index 0000000..c8782de --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Classes/DDAbstractDatabaseLogger.m @@ -0,0 +1,660 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2015, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software 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. +// +// * Neither the name of Deusty nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific +// prior written permission of Deusty, LLC. + +#import "DDAbstractDatabaseLogger.h" +#import + + +#if !__has_feature(objc_arc) +#error This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +@interface DDAbstractDatabaseLogger () + +- (void)destroySaveTimer; +- (void)destroyDeleteTimer; + +@end + +#pragma mark - + +@implementation DDAbstractDatabaseLogger + +- (instancetype)init { + if ((self = [super init])) { + _saveThreshold = 500; + _saveInterval = 60; // 60 seconds + _maxAge = (60 * 60 * 24 * 7); // 7 days + _deleteInterval = (60 * 5); // 5 minutes + } + + return self; +} + +- (void)dealloc { + [self destroySaveTimer]; + [self destroyDeleteTimer]; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Override Me +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (BOOL)db_log:(DDLogMessage *)logMessage { + // Override me and add your implementation. + // + // Return YES if an item was added to the buffer. + // Return NO if the logMessage was ignored. + + return NO; +} + +- (void)db_save { + // Override me and add your implementation. +} + +- (void)db_delete { + // Override me and add your implementation. +} + +- (void)db_saveAndDelete { + // Override me and add your implementation. +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Private API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)performSaveAndSuspendSaveTimer { + if (_unsavedCount > 0) { + if (_deleteOnEverySave) { + [self db_saveAndDelete]; + } else { + [self db_save]; + } + } + + _unsavedCount = 0; + _unsavedTime = 0; + + if (_saveTimer && !_saveTimerSuspended) { + dispatch_suspend(_saveTimer); + _saveTimerSuspended = YES; + } +} + +- (void)performDelete { + if (_maxAge > 0.0) { + [self db_delete]; + + _lastDeleteTime = dispatch_time(DISPATCH_TIME_NOW, 0); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Timers +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)destroySaveTimer { + if (_saveTimer) { + dispatch_source_cancel(_saveTimer); + + if (_saveTimerSuspended) { + // Must resume a timer before releasing it (or it will crash) + dispatch_resume(_saveTimer); + _saveTimerSuspended = NO; + } + + #if !OS_OBJECT_USE_OBJC + dispatch_release(_saveTimer); + #endif + _saveTimer = NULL; + } +} + +- (void)updateAndResumeSaveTimer { + if ((_saveTimer != NULL) && (_saveInterval > 0.0) && (_unsavedTime > 0.0)) { + uint64_t interval = (uint64_t)(_saveInterval * NSEC_PER_SEC); + dispatch_time_t startTime = dispatch_time(_unsavedTime, interval); + + dispatch_source_set_timer(_saveTimer, startTime, interval, 1.0); + + if (_saveTimerSuspended) { + dispatch_resume(_saveTimer); + _saveTimerSuspended = NO; + } + } +} + +- (void)createSuspendedSaveTimer { + if ((_saveTimer == NULL) && (_saveInterval > 0.0)) { + _saveTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.loggerQueue); + + dispatch_source_set_event_handler(_saveTimer, ^{ @autoreleasepool { + [self performSaveAndSuspendSaveTimer]; + } }); + + _saveTimerSuspended = YES; + } +} + +- (void)destroyDeleteTimer { + if (_deleteTimer) { + dispatch_source_cancel(_deleteTimer); + #if !OS_OBJECT_USE_OBJC + dispatch_release(_deleteTimer); + #endif + _deleteTimer = NULL; + } +} + +- (void)updateDeleteTimer { + if ((_deleteTimer != NULL) && (_deleteInterval > 0.0) && (_maxAge > 0.0)) { + uint64_t interval = (uint64_t)(_deleteInterval * NSEC_PER_SEC); + dispatch_time_t startTime; + + if (_lastDeleteTime > 0) { + startTime = dispatch_time(_lastDeleteTime, interval); + } else { + startTime = dispatch_time(DISPATCH_TIME_NOW, interval); + } + + dispatch_source_set_timer(_deleteTimer, startTime, interval, 1.0); + } +} + +- (void)createAndStartDeleteTimer { + if ((_deleteTimer == NULL) && (_deleteInterval > 0.0) && (_maxAge > 0.0)) { + _deleteTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.loggerQueue); + + if (_deleteTimer != NULL) { + dispatch_source_set_event_handler(_deleteTimer, ^{ @autoreleasepool { + [self performDelete]; + } }); + + [self updateDeleteTimer]; + + if (_deleteTimer != NULL) { + dispatch_resume(_deleteTimer); + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Configuration +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (NSUInteger)saveThreshold { + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSUInteger result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(self.loggerQueue, ^{ + result = _saveThreshold; + }); + }); + + return result; +} + +- (void)setSaveThreshold:(NSUInteger)threshold { + dispatch_block_t block = ^{ + @autoreleasepool { + if (_saveThreshold != threshold) { + _saveThreshold = threshold; + + // Since the saveThreshold has changed, + // we check to see if the current unsavedCount has surpassed the new threshold. + // + // If it has, we immediately save the log. + + if ((_unsavedCount >= _saveThreshold) && (_saveThreshold > 0)) { + [self performSaveAndSuspendSaveTimer]; + } + } + } + }; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) { + block(); + } else { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(self.loggerQueue, block); + }); + } +} + +- (NSTimeInterval)saveInterval { + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(self.loggerQueue, ^{ + result = _saveInterval; + }); + }); + + return result; +} + +- (void)setSaveInterval:(NSTimeInterval)interval { + dispatch_block_t block = ^{ + @autoreleasepool { + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* saveInterval != interval */ islessgreater(_saveInterval, interval)) { + _saveInterval = interval; + + // There are several cases we need to handle here. + // + // 1. If the saveInterval was previously enabled and it just got disabled, + // then we need to stop the saveTimer. (And we might as well release it.) + // + // 2. If the saveInterval was previously disabled and it just got enabled, + // then we need to setup the saveTimer. (Plus we might need to do an immediate save.) + // + // 3. If the saveInterval increased, then we need to reset the timer so that it fires at the later date. + // + // 4. If the saveInterval decreased, then we need to reset the timer so that it fires at an earlier date. + // (Plus we might need to do an immediate save.) + + if (_saveInterval > 0.0) { + if (_saveTimer == NULL) { + // Handles #2 + // + // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self createSuspendedSaveTimer]; + [self updateAndResumeSaveTimer]; + } else { + // Handles #3 + // Handles #4 + // + // Since the saveTimer uses the unsavedTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self updateAndResumeSaveTimer]; + } + } else if (_saveTimer) { + // Handles #1 + + [self destroySaveTimer]; + } + } + } + }; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) { + block(); + } else { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(self.loggerQueue, block); + }); + } +} + +- (NSTimeInterval)maxAge { + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(self.loggerQueue, ^{ + result = _maxAge; + }); + }); + + return result; +} + +- (void)setMaxAge:(NSTimeInterval)interval { + dispatch_block_t block = ^{ + @autoreleasepool { + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* maxAge != interval */ islessgreater(_maxAge, interval)) { + NSTimeInterval oldMaxAge = _maxAge; + NSTimeInterval newMaxAge = interval; + + _maxAge = interval; + + // There are several cases we need to handle here. + // + // 1. If the maxAge was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the maxAge was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the maxAge was increased, + // then we don't need to do anything. + // + // 4. If the maxAge was decreased, + // then we should do an immediate delete. + + BOOL shouldDeleteNow = NO; + + if (oldMaxAge > 0.0) { + if (newMaxAge <= 0.0) { + // Handles #1 + + [self destroyDeleteTimer]; + } else if (oldMaxAge > newMaxAge) { + // Handles #4 + shouldDeleteNow = YES; + } + } else if (newMaxAge > 0.0) { + // Handles #2 + shouldDeleteNow = YES; + } + + if (shouldDeleteNow) { + [self performDelete]; + + if (_deleteTimer) { + [self updateDeleteTimer]; + } else { + [self createAndStartDeleteTimer]; + } + } + } + } + }; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) { + block(); + } else { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(self.loggerQueue, block); + }); + } +} + +- (NSTimeInterval)deleteInterval { + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block NSTimeInterval result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(self.loggerQueue, ^{ + result = _deleteInterval; + }); + }); + + return result; +} + +- (void)setDeleteInterval:(NSTimeInterval)interval { + dispatch_block_t block = ^{ + @autoreleasepool { + // C99 recommended floating point comparison macro + // Read: isLessThanOrGreaterThan(floatA, floatB) + + if (/* deleteInterval != interval */ islessgreater(_deleteInterval, interval)) { + _deleteInterval = interval; + + // There are several cases we need to handle here. + // + // 1. If the deleteInterval was previously enabled and it just got disabled, + // then we need to stop the deleteTimer. (And we might as well release it.) + // + // 2. If the deleteInterval was previously disabled and it just got enabled, + // then we need to setup the deleteTimer. (Plus we might need to do an immediate delete.) + // + // 3. If the deleteInterval increased, then we need to reset the timer so that it fires at the later date. + // + // 4. If the deleteInterval decreased, then we need to reset the timer so that it fires at an earlier date. + // (Plus we might need to do an immediate delete.) + + if (_deleteInterval > 0.0) { + if (_deleteTimer == NULL) { + // Handles #2 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a delete is needed the timer will fire immediately. + + [self createAndStartDeleteTimer]; + } else { + // Handles #3 + // Handles #4 + // + // Since the deleteTimer uses the lastDeleteTime to calculate it's first fireDate, + // if a save is needed the timer will fire immediately. + + [self updateDeleteTimer]; + } + } else if (_deleteTimer) { + // Handles #1 + + [self destroyDeleteTimer]; + } + } + } + }; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) { + block(); + } else { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(self.loggerQueue, block); + }); + } +} + +- (BOOL)deleteOnEverySave { + // The design of this method is taken from the DDAbstractLogger implementation. + // For extensive documentation please refer to the DDAbstractLogger implementation. + + // Note: The internal implementation MUST access the colorsEnabled variable directly, + // This method is designed explicitly for external access. + // + // Using "self." syntax to go through this method will cause immediate deadlock. + // This is the intended result. Fix it by accessing the ivar directly. + // Great strides have been take to ensure this is safe to do. Plus it's MUCH faster. + + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + NSAssert(![self isOnInternalLoggerQueue], @"MUST access ivar directly, NOT via self.* syntax."); + + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + + __block BOOL result; + + dispatch_sync(globalLoggingQueue, ^{ + dispatch_sync(self.loggerQueue, ^{ + result = _deleteOnEverySave; + }); + }); + + return result; +} + +- (void)setDeleteOnEverySave:(BOOL)flag { + dispatch_block_t block = ^{ + _deleteOnEverySave = flag; + }; + + // The design of the setter logic below is taken from the DDAbstractLogger implementation. + // For documentation please refer to the DDAbstractLogger implementation. + + if ([self isOnInternalLoggerQueue]) { + block(); + } else { + dispatch_queue_t globalLoggingQueue = [DDLog loggingQueue]; + NSAssert(![self isOnGlobalLoggingQueue], @"Core architecture requirement failure"); + + dispatch_async(globalLoggingQueue, ^{ + dispatch_async(self.loggerQueue, block); + }); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public API +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)savePendingLogEntries { + dispatch_block_t block = ^{ + @autoreleasepool { + [self performSaveAndSuspendSaveTimer]; + } + }; + + if ([self isOnInternalLoggerQueue]) { + block(); + } else { + dispatch_async(self.loggerQueue, block); + } +} + +- (void)deleteOldLogEntries { + dispatch_block_t block = ^{ + @autoreleasepool { + [self performDelete]; + } + }; + + if ([self isOnInternalLoggerQueue]) { + block(); + } else { + dispatch_async(self.loggerQueue, block); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark DDLogger +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +- (void)didAddLogger { + // If you override me be sure to invoke [super didAddLogger]; + + [self createSuspendedSaveTimer]; + + [self createAndStartDeleteTimer]; +} + +- (void)willRemoveLogger { + // If you override me be sure to invoke [super willRemoveLogger]; + + [self performSaveAndSuspendSaveTimer]; + + [self destroySaveTimer]; + [self destroyDeleteTimer]; +} + +- (void)logMessage:(DDLogMessage *)logMessage { + if ([self db_log:logMessage]) { + BOOL firstUnsavedEntry = (++_unsavedCount == 1); + + if ((_unsavedCount >= _saveThreshold) && (_saveThreshold > 0)) { + [self performSaveAndSuspendSaveTimer]; + } else if (firstUnsavedEntry) { + _unsavedTime = dispatch_time(DISPATCH_TIME_NOW, 0); + [self updateAndResumeSaveTimer]; + } + } +} + +- (void)flush { + // This method is invoked by DDLog's flushLog method. + // + // It is called automatically when the application quits, + // or if the developer invokes DDLog's flushLog method prior to crashing or something. + + [self performSaveAndSuspendSaveTimer]; +} + +@end diff --git a/Example/Pods/CocoaLumberjack/Classes/DDAssertMacros.h b/Example/Pods/CocoaLumberjack/Classes/DDAssertMacros.h new file mode 100644 index 0000000..870d31f --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Classes/DDAssertMacros.h @@ -0,0 +1,26 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2015, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software 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. +// +// * Neither the name of Deusty nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific +// prior written permission of Deusty, LLC. + +/** + * NSAsset replacement that will output a log message even when assertions are disabled. + **/ +#define DDAssert(condition, frmt, ...) \ + if (!(condition)) { \ + NSString *description = [NSString stringWithFormat:frmt, ## __VA_ARGS__]; \ + DDLogError(@"%@", description); \ + NSAssert(NO, description); \ + } +#define DDAssertCondition(condition) DDAssert(condition, @"Condition not satisfied: %s", #condition) + diff --git a/Example/Pods/CocoaLumberjack/Classes/DDFileLogger.h b/Example/Pods/CocoaLumberjack/Classes/DDFileLogger.h new file mode 100644 index 0000000..f0bfdb6 --- /dev/null +++ b/Example/Pods/CocoaLumberjack/Classes/DDFileLogger.h @@ -0,0 +1,487 @@ +// Software License Agreement (BSD License) +// +// Copyright (c) 2010-2015, Deusty, LLC +// All rights reserved. +// +// Redistribution and use of this software 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. +// +// * Neither the name of Deusty nor the names of its contributors may be used +// to endorse or promote products derived from this software without specific +// prior written permission of Deusty, LLC. + +// Disable legacy macros +#ifndef DD_LEGACY_MACROS + #define DD_LEGACY_MACROS 0 +#endif + +#import "DDLog.h" + +@class DDLogFileInfo; + +/** + * This class provides a logger to write log statements to a file. + **/ + + +// Default configuration and safety/sanity values. +// +// maximumFileSize -> kDDDefaultLogMaxFileSize +// rollingFrequency -> kDDDefaultLogRollingFrequency +// maximumNumberOfLogFiles -> kDDDefaultLogMaxNumLogFiles +// logFilesDiskQuota -> kDDDefaultLogFilesDiskQuota +// +// You should carefully consider the proper configuration values for your application. + +extern unsigned long long const kDDDefaultLogMaxFileSize; +extern NSTimeInterval const kDDDefaultLogRollingFrequency; +extern NSUInteger const kDDDefaultLogMaxNumLogFiles; +extern unsigned long long const kDDDefaultLogFilesDiskQuota; + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * The LogFileManager protocol is designed to allow you to control all aspects of your log files. + * + * The primary purpose of this is to allow you to do something with the log files after they have been rolled. + * Perhaps you want to compress them to save disk space. + * Perhaps you want to upload them to an FTP server. + * Perhaps you want to run some analytics on the file. + * + * A default LogFileManager is, of course, provided. + * The default LogFileManager simply deletes old log files according to the maximumNumberOfLogFiles property. + * + * This protocol provides various methods to fetch the list of log files. + * + * There are two variants: sorted and unsorted. + * If sorting is not necessary, the unsorted variant is obviously faster. + * The sorted variant will return an array sorted by when the log files were created, + * with the most recently created log file at index 0, and the oldest log file at the end of the array. + * + * You can fetch only the log file paths (full path including name), log file names (name only), + * or an array of `DDLogFileInfo` objects. + * The `DDLogFileInfo` class is documented below, and provides a handy wrapper that + * gives you easy access to various file attributes such as the creation date or the file size. + */ +@protocol DDLogFileManager +@required + +// Public properties + +/** + * The maximum number of archived log files to keep on disk. + * For example, if this property is set to 3, + * then the LogFileManager will only keep 3 archived log files (plus the current active log file) on disk. + * Once the active log file is rolled/archived, then the oldest of the existing 3 rolled/archived log files is deleted. + * + * You may optionally disable this option by setting it to zero. + **/ +@property (readwrite, assign, atomic) NSUInteger maximumNumberOfLogFiles; + +/** + * The maximum space that logs can take. On rolling logfile all old logfiles that exceed logFilesDiskQuota will + * be deleted. + * + * You may optionally disable this option by setting it to zero. + **/ +@property (readwrite, assign, atomic) unsigned long long logFilesDiskQuota; + +// Public methods + +/** + * Returns the logs directory (path) + */ +- (NSString *)logsDirectory; + +/** + * Returns an array of `NSString` objects, + * each of which is the filePath to an existing log file on disk. + **/ +- (NSArray *)unsortedLogFilePaths; + +/** + * Returns an array of `NSString` objects, + * each of which is the fileName of an existing log file on disk. + **/ +- (NSArray *)unsortedLogFileNames; + +/** + * Returns an array of `DDLogFileInfo` objects, + * each representing an existing log file on disk, + * and containing important information about the log file such as it's modification date and size. + **/ +- (NSArray *)unsortedLogFileInfos; + +/** + * Just like the `unsortedLogFilePaths` method, but sorts the array. + * The items in the array are sorted by creation date. + * The first item in the array will be the most recently created log file. + **/ +- (NSArray *)sortedLogFilePaths; + +/** + * Just like the `unsortedLogFileNames` method, but sorts the array. + * The items in the array are sorted by creation date. + * The first item in the array will be the most recently created log file. + **/ +- (NSArray *)sortedLogFileNames; + +/** + * Just like the `unsortedLogFileInfos` method, but sorts the array. + * The items in the array are sorted by creation date. + * The first item in the array will be the most recently created log file. + **/ +- (NSArray *)sortedLogFileInfos; + +// Private methods (only to be used by DDFileLogger) + +/** + * Generates a new unique log file path, and creates the corresponding log file. + **/ +- (NSString *)createNewLogFile; + +@optional + +// Notifications from DDFileLogger + +/** + * Called when a log file was archieved + */ +- (void)didArchiveLogFile:(NSString *)logFilePath; + +/** + * Called when the roll action was executed and the log was archieved + */ +- (void)didRollAndArchiveLogFile:(NSString *)logFilePath; + +@end + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * Default log file manager. + * + * All log files are placed inside the logsDirectory. + * If a specific logsDirectory isn't specified, the default directory is used. + * On Mac, this is in `~/Library/Logs/`. + * On iPhone, this is in `~/Library/Caches/Logs`. + * + * Log files are named `"