diff --git a/.eslintrc b/.eslintrc index a81fb56f3..da893862d 100644 --- a/.eslintrc +++ b/.eslintrc @@ -16,6 +16,7 @@ "document": false, "escape": false, "exports": false, + "fetch": false, "global": false, "jest": false, "Map": true, diff --git a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj index 3928adc33..adaa6f58b 100644 --- a/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj +++ b/Examples/SampleApp/SampleApp.xcodeproj/project.pbxproj @@ -7,7 +7,6 @@ objects = { /* Begin PBXBuildFile section */ - 00481BE81AC0C86700671115 /* libRCTWebSocketDebugger.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00481BE61AC0C7FA00671115 /* libRCTWebSocketDebugger.a */; }; 008F07F31AC5B25A0029DE68 /* main.jsbundle in Resources */ = {isa = PBXBuildFile; fileRef = 008F07F21AC5B25A0029DE68 /* main.jsbundle */; }; 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; 00C302E61ABCBA2D00DB3ED1 /* libRCTAdSupport.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302B41ABCB8E700DB3ED1 /* libRCTAdSupport.a */; }; @@ -27,13 +26,6 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 00481BE51AC0C7FA00671115 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 00481BDB1AC0C7FA00671115 /* RCTWebSocketDebugger.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; - remoteInfo = RCTWebSocketDebugger; - }; 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; @@ -114,7 +106,6 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ - 00481BDB1AC0C7FA00671115 /* RCTWebSocketDebugger.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocketDebugger.xcodeproj; path = ../../Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj; sourceTree = ""; }; 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = main.jsbundle; path = iOS/main.jsbundle; sourceTree = ""; }; 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../../Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; }; 00C302AF1ABCB8E700DB3ED1 /* RCTAdSupport.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAdSupport.xcodeproj; path = ../../Libraries/AdSupport/RCTAdSupport.xcodeproj; sourceTree = ""; }; @@ -151,30 +142,21 @@ buildActionMask = 2147483647; files = ( 146834051AC3E58100842450 /* libReact.a in Frameworks */, - 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */, - 00481BE81AC0C86700671115 /* libRCTWebSocketDebugger.a in Frameworks */, 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, 00C302E61ABCBA2D00DB3ED1 /* libRCTAdSupport.a in Frameworks */, 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */, 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */, 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */, 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */, - 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, + 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */, 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, + 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 00481BDC1AC0C7FA00671115 /* Products */ = { - isa = PBXGroup; - children = ( - 00481BE61AC0C7FA00671115 /* libRCTWebSocketDebugger.a */, - ); - name = Products; - sourceTree = ""; - }; 00C302A81ABCB8CE00DB3ED1 /* Products */ = { isa = PBXGroup; children = ( @@ -291,7 +273,6 @@ 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */, 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, - 00481BDB1AC0C7FA00671115 /* RCTWebSocketDebugger.xcodeproj */, ); name = Libraries; sourceTree = ""; @@ -426,10 +407,6 @@ ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */; ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; }, - { - ProductGroup = 00481BDC1AC0C7FA00671115 /* Products */; - ProjectRef = 00481BDB1AC0C7FA00671115 /* RCTWebSocketDebugger.xcodeproj */; - }, { ProductGroup = 146834001AC3E56700842450 /* Products */; ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */; @@ -444,13 +421,6 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ - 00481BE61AC0C7FA00671115 /* libRCTWebSocketDebugger.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTWebSocketDebugger.a; - remoteRef = 00481BE51AC0C7FA00671115 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Examples/UIExplorer/ExampleTypes.js b/Examples/UIExplorer/ExampleTypes.js index 7f6ff08d8..44db06a6c 100644 --- a/Examples/UIExplorer/ExampleTypes.js +++ b/Examples/UIExplorer/ExampleTypes.js @@ -27,4 +27,3 @@ export type ExampleModule = { examples: Array; external?: bool; }; - diff --git a/Examples/UIExplorer/MapViewExample.js b/Examples/UIExplorer/MapViewExample.js index e8dae25ac..5df00236d 100644 --- a/Examples/UIExplorer/MapViewExample.js +++ b/Examples/UIExplorer/MapViewExample.js @@ -29,7 +29,7 @@ var regionText = { longitude: '0', latitudeDelta: '0', longitudeDelta: '0', -} +}; var MapRegionInput = React.createClass({ diff --git a/Examples/UIExplorer/NavigatorIOSColorsExample.js b/Examples/UIExplorer/NavigatorIOSColorsExample.js index 386586f23..a045807c7 100644 --- a/Examples/UIExplorer/NavigatorIOSColorsExample.js +++ b/Examples/UIExplorer/NavigatorIOSColorsExample.js @@ -45,7 +45,7 @@ var NavigatorIOSColors = React.createClass({ render: function() { // Set StatusBar with light contents to get better contrast - StatusBarIOS.setStyle(StatusBarIOS.Style['lightContent']); + StatusBarIOS.setStyle(StatusBarIOS.Style.lightContent); return ( {this._renderContent('#21551C', 'Green Tab')} diff --git a/Examples/UIExplorer/TextInputExample.js b/Examples/UIExplorer/TextInputExample.js index 922dd9607..10064491d 100644 --- a/Examples/UIExplorer/TextInputExample.js +++ b/Examples/UIExplorer/TextInputExample.js @@ -387,7 +387,7 @@ exports.examples = [ - ) + ); } } ]; diff --git a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj index 698fd3b3f..8b94a27cf 100644 --- a/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj +++ b/Examples/UIExplorer/UIExplorer.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 004D28A31AAF61C70097A701 /* UIExplorerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 004D28A21AAF61C70097A701 /* UIExplorerTests.m */; }; - 00D2771A1AB8C3E100DC1E48 /* libRCTWebSocketDebugger.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00D277131AB8C2C700DC1E48 /* libRCTWebSocketDebugger.a */; }; 13417FE91AA91432003F314A /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13417FE81AA91428003F314A /* libRCTImage.a */; }; 134180011AA9153C003F314A /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 13417FEF1AA914B8003F314A /* libRCTText.a */; }; 1341802C1AA9178B003F314A /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1341802B1AA91779003F314A /* libRCTNetwork.a */; }; @@ -34,13 +33,6 @@ remoteGlobalIDString = 13B07F861A680F5B00A75B9A; remoteInfo = UIExplorer; }; - 00D277121AB8C2C700DC1E48 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 00D2770E1AB8C2C700DC1E48 /* RCTWebSocketDebugger.xcodeproj */; - proxyType = 2; - remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; - remoteInfo = RCTWebSocketDebugger; - }; 13417FE71AA91428003F314A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 13417FE31AA91428003F314A /* RCTImage.xcodeproj */; @@ -124,7 +116,6 @@ 004D289E1AAF61C70097A701 /* UIExplorerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIExplorerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 004D28A11AAF61C70097A701 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 004D28A21AAF61C70097A701 /* UIExplorerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UIExplorerTests.m; sourceTree = ""; }; - 00D2770E1AB8C2C700DC1E48 /* RCTWebSocketDebugger.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocketDebugger.xcodeproj; path = ../../Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj; sourceTree = ""; }; 13417FE31AA91428003F314A /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = ../../Libraries/Image/RCTImage.xcodeproj; sourceTree = ""; }; 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../../Libraries/Text/RCTText.xcodeproj; sourceTree = ""; }; 134180261AA91779003F314A /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = ../../Libraries/Network/RCTNetwork.xcodeproj; sourceTree = ""; }; @@ -159,7 +150,6 @@ files = ( 834C36EC1AF8DED70019C93C /* libRCTSettings.a in Frameworks */, 14AADF051AC3DBB1002390C9 /* libReact.a in Frameworks */, - 00D2771A1AB8C3E100DC1E48 /* libRCTWebSocketDebugger.a in Frameworks */, 58005BF21ABA80A60062E044 /* libRCTTest.a in Frameworks */, D85B829E1AB6D5D7003F4FE2 /* libRCTVibration.a in Frameworks */, 14DC67F41AB71881001358AB /* libRCTPushNotification.a in Frameworks */, @@ -192,14 +182,6 @@ name = "Supporting Files"; sourceTree = ""; }; - 00D2770F1AB8C2C700DC1E48 /* Products */ = { - isa = PBXGroup; - children = ( - 00D277131AB8C2C700DC1E48 /* libRCTWebSocketDebugger.a */, - ); - name = Products; - sourceTree = ""; - }; 1316A21D1AA397F400C0188E /* Libraries */ = { isa = PBXGroup; children = ( @@ -213,7 +195,6 @@ 13CC9D481AEED2B90020D1C2 /* RCTSettings.xcodeproj */, 58005BE41ABA80530062E044 /* RCTTest.xcodeproj */, 13417FEA1AA914B8003F314A /* RCTText.xcodeproj */, - 00D2770E1AB8C2C700DC1E48 /* RCTWebSocketDebugger.xcodeproj */, D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */, ); name = Libraries; @@ -446,10 +427,6 @@ ProductGroup = D85B82921AB6D5CE003F4FE2 /* Products */; ProjectRef = D85B82911AB6D5CE003F4FE2 /* RCTVibration.xcodeproj */; }, - { - ProductGroup = 00D2770F1AB8C2C700DC1E48 /* Products */; - ProjectRef = 00D2770E1AB8C2C700DC1E48 /* RCTWebSocketDebugger.xcodeproj */; - }, { ProductGroup = 14AADF001AC3DB95002390C9 /* Products */; ProjectRef = 14AADEFF1AC3DB95002390C9 /* React.xcodeproj */; @@ -464,13 +441,6 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ - 00D277131AB8C2C700DC1E48 /* libRCTWebSocketDebugger.a */ = { - isa = PBXReferenceProxy; - fileType = archive.ar; - path = libRCTWebSocketDebugger.a; - remoteRef = 00D277121AB8C2C700DC1E48 /* PBXContainerItemProxy */; - sourceTree = BUILT_PRODUCTS_DIR; - }; 13417FE81AA91428003F314A /* libRCTImage.a */ = { isa = PBXReferenceProxy; fileType = archive.ar; diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Contents.json b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Contents.json index 413d60e76..6654cff6e 100644 --- a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Contents.json @@ -3,37 +3,37 @@ { "size" : "29x29", "idiom" : "iphone", - "filename" : "uie_icon@2x.png", + "filename" : "Icon-Small@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", - "filename" : "uie_icon@2x-1.png", + "filename" : "Icon-Small@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", - "filename" : "uie_icon@2x-2.png", + "filename" : "Icon-40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", - "filename" : "uie_icon@2x-3.png", + "filename" : "Icon-40@3x.png", "scale" : "3x" }, { "size" : "60x60", "idiom" : "iphone", - "filename" : "uie_icon@2x-5.png", + "filename" : "Icon-60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", - "filename" : "uie_icon@2x-4.png", + "filename" : "Icon-60@3x.png", "scale" : "3x" } ], @@ -41,4 +41,4 @@ "version" : 1, "author" : "xcode" } -} \ No newline at end of file +} diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png new file mode 100644 index 000000000..d4aa4cd71 Binary files /dev/null and b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-40@2x.png differ diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-40@3x.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-40@3x.png new file mode 100644 index 000000000..8f9ee3c9c Binary files /dev/null and b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-40@3x.png differ diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png new file mode 100644 index 000000000..8f9ee3c9c Binary files /dev/null and b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-60@2x.png differ diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x-1.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png similarity index 100% rename from Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x-1.png rename to Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-60@3x.png diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png new file mode 100644 index 000000000..15fb96f88 Binary files /dev/null and b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-Small@2x.png differ diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png new file mode 100644 index 000000000..959b5eae8 Binary files /dev/null and b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/Icon-Small@3x.png differ diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x-2.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x-2.png deleted file mode 100644 index 08a42699d..000000000 Binary files a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x-2.png and /dev/null differ diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x-3.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x-3.png deleted file mode 100644 index 08a42699d..000000000 Binary files a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x-3.png and /dev/null differ diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x-4.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x-4.png deleted file mode 100644 index 08a42699d..000000000 Binary files a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x-4.png and /dev/null differ diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x-5.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x-5.png deleted file mode 100644 index 08a42699d..000000000 Binary files a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x-5.png and /dev/null differ diff --git a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x.png b/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x.png deleted file mode 100644 index 08a42699d..000000000 Binary files a/Examples/UIExplorer/UIExplorer/Images.xcassets/AppIcon.appiconset/uie_icon@2x.png and /dev/null differ diff --git a/Libraries/Components/StaticRenderer.js b/Libraries/Components/StaticRenderer.js index 8800198a5..8f6cabde5 100644 --- a/Libraries/Components/StaticRenderer.js +++ b/Libraries/Components/StaticRenderer.js @@ -6,7 +6,7 @@ * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * - * @provides StaticRenderer + * @providesModule StaticRenderer * @flow */ 'use strict'; diff --git a/Libraries/CustomComponents/ListView/ListView.js b/Libraries/CustomComponents/ListView/ListView.js index 0a03bd38b..abec1a6e0 100644 --- a/Libraries/CustomComponents/ListView/ListView.js +++ b/Libraries/CustomComponents/ListView/ListView.js @@ -87,7 +87,7 @@ var SCROLLVIEW_REF = 'listviewscroll'; * smoothly while dynamically loading potentially very large (or conceptually * infinite) data sets: * - * * Only re-render changed rows - the hasRowChanged function provided to the + * * Only re-render changed rows - the rowHasChanged function provided to the * data source tells the ListView if it needs to re-render a row because the * source data has changed - see ListViewDataSource for more details. * diff --git a/Libraries/CustomComponents/Navigator/Navigator.js b/Libraries/CustomComponents/Navigator/Navigator.js index bc044f1b9..b8c019e32 100644 --- a/Libraries/CustomComponents/Navigator/Navigator.js +++ b/Libraries/CustomComponents/Navigator/Navigator.js @@ -1173,7 +1173,11 @@ var Navigator = React.createClass({ resetTo: function(route) { invariant(!!route, 'Must supply route to push'); this.replaceAtIndex(route, 0, () => { - this.popToRoute(route); + // Do not use popToRoute here, because race conditions could prevent the + // route from existing at this time. Instead, just go to index 0 + if (this.state.presentedIndex > 0) { + this._popN(this.state.presentedIndex); + } }); }, diff --git a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js index 91afe2008..224ce0966 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js +++ b/Libraries/JavaScriptAppEngine/Initialization/ExceptionsManager.js @@ -48,8 +48,10 @@ function reportException(e: Exception, isFatal: bool, stack?: any) { var prettyStack = parseErrorStack(e, map); RCTExceptionsManager.updateExceptionMessage(e.message, prettyStack); }) - .then(null, error => { - console.error('#CLOWNTOWN (error while displaying error): ' + error.message); + .catch(error => { + // This can happen in a variety of normal situations, such as + // Network module not being available, or when running locally + console.warn('Unable to load source map: ' + error.message); }); } } diff --git a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js index 217fd93e7..ec12a99e6 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js +++ b/Libraries/JavaScriptAppEngine/Initialization/InitializeJavaScriptAppEngine.js @@ -33,39 +33,6 @@ if (typeof window === 'undefined') { window = GLOBAL; } -/** - * The document must be shimmed before anything else that might define the - * `ExecutionEnvironment` module (which checks for `document.createElement`). - */ -function setupDocumentShim() { - // The browser defines Text and Image globals by default. If you forget to - // require them, then the error message is very confusing. - function getInvalidGlobalUseError(name) { - return new Error( - 'You are trying to render the global ' + name + ' variable as a ' + - 'React element. You probably forgot to require ' + name + '.' - ); - } - GLOBAL.Text = { - get defaultProps() { - throw getInvalidGlobalUseError('Text'); - } - }; - GLOBAL.Image = { - get defaultProps() { - throw getInvalidGlobalUseError('Image'); - } - }; - // Force `ExecutionEnvironment.canUseDOM` to be false. - if (GLOBAL.document) { - GLOBAL.document.createElement = null; - } - - // There is no DOM so MutationObserver doesn't make sense. It is used - // as feature detection in Bluebird Promise implementation - GLOBAL.MutationObserver = undefined; -} - function handleErrorWithRedBox(e, isFatal) { try { require('ExceptionsManager').handleException(e, isFatal); @@ -74,12 +41,12 @@ function handleErrorWithRedBox(e, isFatal) { } } -function setupRedBoxErrorHandler() { +function setUpRedBoxErrorHandler() { var ErrorUtils = require('ErrorUtils'); ErrorUtils.setGlobalHandler(handleErrorWithRedBox); } -function setupRedBoxConsoleErrorHandler() { +function setUpRedBoxConsoleErrorHandler() { // ExceptionsManager transitively requires Promise so we install it after var ExceptionsManager = require('ExceptionsManager'); var Platform = require('Platform'); @@ -96,7 +63,7 @@ function setupRedBoxConsoleErrorHandler() { * implement our own custom timing bridge that should be immune to * unexplainably dropped timing signals. */ -function setupTimers() { +function setUpTimers() { var JSTimers = require('JSTimers'); GLOBAL.setTimeout = JSTimers.setTimeout; GLOBAL.setInterval = JSTimers.setInterval; @@ -111,7 +78,7 @@ function setupTimers() { }; } -function setupAlert() { +function setUpAlert() { var RCTAlertManager = require('NativeModules').AlertManager; if (!GLOBAL.alert) { GLOBAL.alert = function(text) { @@ -125,13 +92,13 @@ function setupAlert() { } } -function setupPromise() { +function setUpPromise() { // The native Promise implementation throws the following error: // ERROR: Event loop not supported. GLOBAL.Promise = require('Promise'); } -function setupXHR() { +function setUpXHR() { // The native XMLHttpRequest in Chrome dev tools is CORS aware and won't // let you fetch anything from the internet GLOBAL.XMLHttpRequest = require('XMLHttpRequest'); @@ -143,16 +110,20 @@ function setupXHR() { GLOBAL.Response = fetchPolyfill.Response; } -function setupGeolocation() { +function setUpGeolocation() { GLOBAL.navigator = GLOBAL.navigator || {}; GLOBAL.navigator.geolocation = require('Geolocation'); } -setupDocumentShim(); -setupRedBoxErrorHandler(); -setupTimers(); -setupAlert(); -setupPromise(); -setupXHR(); -setupRedBoxConsoleErrorHandler(); -setupGeolocation(); +function setUpWebSockets() { + GLOBAL.WebSocket = require('WebSocket'); +} + +setUpRedBoxErrorHandler(); +setUpTimers(); +setUpAlert(); +setUpPromise(); +setUpXHR(); +setUpRedBoxConsoleErrorHandler(); +setUpGeolocation(); +setUpWebSockets(); diff --git a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js index a826db7bf..c3499ac9f 100644 --- a/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js +++ b/Libraries/JavaScriptAppEngine/Initialization/loadSourceMap.js @@ -13,10 +13,13 @@ 'use strict'; var Promise = require('Promise'); -var RCTSourceCode = require('NativeModules').SourceCode; +var NativeModules = require('NativeModules'); var SourceMapConsumer = require('SourceMap').SourceMapConsumer; var SourceMapURL = require('./source-map-url'); +var RCTSourceCode = NativeModules.SourceCode; +var RCTDataManager = NativeModules.DataManager; + function loadSourceMap(): Promise { return fetchSourceMap() .then(map => new SourceMapConsumer(map)); @@ -31,17 +34,31 @@ function fetchSourceMap(): Promise { return Promise.reject(new Error('RCTSourceCode module is not available')); } + if (!RCTDataManager) { + // Used internally by fetch + return Promise.reject(new Error('RCTDataManager module is not available')); + } + return new Promise(RCTSourceCode.getScriptText) .then(extractSourceMapURL) + .then((url) => { + if (url === null) { + return Promise.reject(new Error('No source map URL found. May be running from bundled file.')); + } + return Promise.resolve(url); + }) .then(fetch) .then(response => response.text()) } -function extractSourceMapURL({url, text, fullSourceMappingURL}): string { +function extractSourceMapURL({url, text, fullSourceMappingURL}): ?string { if (fullSourceMappingURL) { return fullSourceMappingURL; } var mapURL = SourceMapURL.getFrom(text); + if (!mapURL) { + return null; + } var baseURL = url.match(/(.+:\/\/.*?)\//)[1]; return baseURL + mapURL; } diff --git a/Libraries/JavaScriptAppEngine/polyfills/document.js b/Libraries/JavaScriptAppEngine/polyfills/document.js new file mode 100644 index 000000000..4e76db957 --- /dev/null +++ b/Libraries/JavaScriptAppEngine/polyfills/document.js @@ -0,0 +1,34 @@ +/* eslint global-strict: 0 */ +(function(GLOBAL) { + /** + * The document must be shimmed before anything else that might define the + * `ExecutionEnvironment` module (which checks for `document.createElement`). + */ + + // The browser defines Text and Image globals by default. If you forget to + // require them, then the error message is very confusing. + function getInvalidGlobalUseError(name) { + return new Error( + 'You are trying to render the global ' + name + ' variable as a ' + + 'React element. You probably forgot to require ' + name + '.' + ); + } + GLOBAL.Text = { + get defaultProps() { + throw getInvalidGlobalUseError('Text'); + } + }; + GLOBAL.Image = { + get defaultProps() { + throw getInvalidGlobalUseError('Image'); + } + }; + // Force `ExecutionEnvironment.canUseDOM` to be false. + if (GLOBAL.document) { + GLOBAL.document.createElement = null; + } + + // There is no DOM so MutationObserver doesn't make sense. It is used + // as feature detection in Bluebird Promise implementation + GLOBAL.MutationObserver = undefined; +})(this); diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj b/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj deleted file mode 100644 index 38ac20c73..000000000 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketDebugger.xcodeproj/project.pbxproj +++ /dev/null @@ -1,275 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 00D277161AB8C32C00DC1E48 /* RCTWebSocketExecutor.m in Sources */ = {isa = PBXBuildFile; fileRef = 00D277151AB8C32C00DC1E48 /* RCTWebSocketExecutor.m */; }; - 13AF20421AE707C5005F5298 /* SRWebSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 13AF20411AE707C5005F5298 /* SRWebSocket.m */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 832C817E1AAF6DEF007FA2F7 /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "include/$(PRODUCT_NAME)"; - dstSubfolderSpec = 16; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 00D277141AB8C32C00DC1E48 /* RCTWebSocketExecutor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTWebSocketExecutor.h; sourceTree = ""; }; - 00D277151AB8C32C00DC1E48 /* RCTWebSocketExecutor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTWebSocketExecutor.m; sourceTree = ""; }; - 13AF20401AE707C5005F5298 /* SRWebSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SRWebSocket.h; sourceTree = ""; }; - 13AF20411AE707C5005F5298 /* SRWebSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SRWebSocket.m; sourceTree = ""; }; - 832C81801AAF6DEF007FA2F7 /* libRCTWebSocketDebugger.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTWebSocketDebugger.a; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 832C817D1AAF6DEF007FA2F7 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 832C81771AAF6DEF007FA2F7 = { - isa = PBXGroup; - children = ( - 13AF20401AE707C5005F5298 /* SRWebSocket.h */, - 13AF20411AE707C5005F5298 /* SRWebSocket.m */, - 00D277141AB8C32C00DC1E48 /* RCTWebSocketExecutor.h */, - 00D277151AB8C32C00DC1E48 /* RCTWebSocketExecutor.m */, - 832C81811AAF6DEF007FA2F7 /* Products */, - ); - indentWidth = 2; - sourceTree = ""; - tabWidth = 2; - }; - 832C81811AAF6DEF007FA2F7 /* Products */ = { - isa = PBXGroup; - children = ( - 832C81801AAF6DEF007FA2F7 /* libRCTWebSocketDebugger.a */, - ); - name = Products; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 832C817F1AAF6DEF007FA2F7 /* RCTWebSocketDebugger */ = { - isa = PBXNativeTarget; - buildConfigurationList = 832C81941AAF6DF0007FA2F7 /* Build configuration list for PBXNativeTarget "RCTWebSocketDebugger" */; - buildPhases = ( - 832C817C1AAF6DEF007FA2F7 /* Sources */, - 832C817D1AAF6DEF007FA2F7 /* Frameworks */, - 832C817E1AAF6DEF007FA2F7 /* CopyFiles */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = RCTWebSocketDebugger; - productName = RCTWebSocketDebugger; - productReference = 832C81801AAF6DEF007FA2F7 /* libRCTWebSocketDebugger.a */; - productType = "com.apple.product-type.library.static"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 832C81781AAF6DEF007FA2F7 /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 0620; - ORGANIZATIONNAME = Facebook; - TargetAttributes = { - 832C817F1AAF6DEF007FA2F7 = { - CreatedOnToolsVersion = 6.2; - }; - }; - }; - buildConfigurationList = 832C817B1AAF6DEF007FA2F7 /* Build configuration list for PBXProject "RCTWebSocketDebugger" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; - hasScannedForEncodings = 0; - knownRegions = ( - en, - ); - mainGroup = 832C81771AAF6DEF007FA2F7; - productRefGroup = 832C81811AAF6DEF007FA2F7 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 832C817F1AAF6DEF007FA2F7 /* RCTWebSocketDebugger */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXSourcesBuildPhase section */ - 832C817C1AAF6DEF007FA2F7 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 13AF20421AE707C5005F5298 /* SRWebSocket.m in Sources */, - 00D277161AB8C32C00DC1E48 /* RCTWebSocketExecutor.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin XCBuildConfiguration section */ - 832C81921AAF6DF0007FA2F7 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_SYMBOLS_PRIVATE_EXTERN = NO; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - HEADER_SEARCH_PATHS = ( - "$(inherited)", - /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, - "$(SRCROOT)/../../React/**", - ); - IPHONEOS_DEPLOYMENT_TARGET = 8.2; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - }; - name = Debug; - }; - 832C81931AAF6DF0007FA2F7 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - COPY_PHASE_STRIP = NO; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - HEADER_SEARCH_PATHS = ( - "$(inherited)", - /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, - "$(SRCROOT)/../../React/**", - ); - IPHONEOS_DEPLOYMENT_TARGET = 8.2; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 832C81951AAF6DF0007FA2F7 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_STATIC_ANALYZER_MODE = deep; - HEADER_SEARCH_PATHS = ( - "$(inherited)", - /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, - "$(SRCROOT)/../../React/**", - ); - OTHER_LDFLAGS = ( - "-ObjC", - "-llibicucore", - ); - PRODUCT_NAME = "$(TARGET_NAME)"; - RUN_CLANG_STATIC_ANALYZER = YES; - SKIP_INSTALL = YES; - }; - name = Debug; - }; - 832C81961AAF6DF0007FA2F7 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CLANG_STATIC_ANALYZER_MODE = deep; - HEADER_SEARCH_PATHS = ( - "$(inherited)", - /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, - "$(SRCROOT)/../../React/**", - ); - OTHER_LDFLAGS = ( - "-ObjC", - "-llibicucore", - ); - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 832C817B1AAF6DEF007FA2F7 /* Build configuration list for PBXProject "RCTWebSocketDebugger" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 832C81921AAF6DF0007FA2F7 /* Debug */, - 832C81931AAF6DF0007FA2F7 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 832C81941AAF6DF0007FA2F7 /* Build configuration list for PBXNativeTarget "RCTWebSocketDebugger" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 832C81951AAF6DF0007FA2F7 /* Debug */, - 832C81961AAF6DF0007FA2F7 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 832C81781AAF6DEF007FA2F7 /* Project object */; -} diff --git a/Libraries/RCTWebSocketDebugger/SRWebSocket.h b/Libraries/RCTWebSocketDebugger/SRWebSocket.h deleted file mode 100644 index 5cce725a3..000000000 --- a/Libraries/RCTWebSocketDebugger/SRWebSocket.h +++ /dev/null @@ -1,132 +0,0 @@ -// -// Copyright 2012 Square Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#import -#import - -typedef enum { - SR_CONNECTING = 0, - SR_OPEN = 1, - SR_CLOSING = 2, - SR_CLOSED = 3, -} SRReadyState; - -typedef enum SRStatusCode : NSInteger { - SRStatusCodeNormal = 1000, - SRStatusCodeGoingAway = 1001, - SRStatusCodeProtocolError = 1002, - SRStatusCodeUnhandledType = 1003, - // 1004 reserved. - SRStatusNoStatusReceived = 1005, - // 1004-1006 reserved. - SRStatusCodeInvalidUTF8 = 1007, - SRStatusCodePolicyViolated = 1008, - SRStatusCodeMessageTooBig = 1009, -} SRStatusCode; - -@class SRWebSocket; - -extern NSString *const SRWebSocketErrorDomain; -extern NSString *const SRHTTPResponseErrorKey; - -#pragma mark - SRWebSocketDelegate - -@protocol SRWebSocketDelegate; - -#pragma mark - SRWebSocket - -@interface SRWebSocket : NSObject - -@property (nonatomic, weak) id delegate; - -@property (nonatomic, readonly) SRReadyState readyState; -@property (nonatomic, readonly, retain) NSURL *url; - -// This returns the negotiated protocol. -// It will be nil until after the handshake completes. -@property (nonatomic, readonly, copy) NSString *protocol; - -// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol. -- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; -- (id)initWithURLRequest:(NSURLRequest *)request; - -// Some helper constructors. -- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; -- (id)initWithURL:(NSURL *)url; - -// Delegate queue will be dispatch_main_queue by default. -// You cannot set both OperationQueue and dispatch_queue. -- (void)setDelegateOperationQueue:(NSOperationQueue*) queue; -- (void)setDelegateDispatchQueue:(dispatch_queue_t) queue; - -// By default, it will schedule itself on +[NSRunLoop SR_networkRunLoop] using defaultModes. -- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; -- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; - -// SRWebSockets are intended for one-time-use only. Open should be called once and only once. -- (void)open; - -- (void)close; -- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason; - -// Send a UTF8 String or Data. -- (void)send:(id)data; - -// Send Data (can be nil) in a ping message. -- (void)sendPing:(NSData *)data; - -@end - -#pragma mark - SRWebSocketDelegate - -@protocol SRWebSocketDelegate - -// message will either be an NSString if the server is using text -// or NSData if the server is using binary. -- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message; - -@optional - -- (void)webSocketDidOpen:(SRWebSocket *)webSocket; -- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error; -- (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean; -- (void)webSocket:(SRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload; - -@end - -#pragma mark - NSURLRequest (CertificateAdditions) - -@interface NSURLRequest (CertificateAdditions) - -@property (nonatomic, retain, readonly) NSArray *SR_SSLPinnedCertificates; - -@end - -#pragma mark - NSMutableURLRequest (CertificateAdditions) - -@interface NSMutableURLRequest (CertificateAdditions) - -@property (nonatomic, retain) NSArray *SR_SSLPinnedCertificates; - -@end - -#pragma mark - NSRunLoop (SRWebSocket) - -@interface NSRunLoop (SRWebSocket) - -+ (NSRunLoop *)SR_networkRunLoop; - -@end diff --git a/Libraries/RCTWebSocketDebugger/SRWebSocket.m b/Libraries/RCTWebSocketDebugger/SRWebSocket.m deleted file mode 100644 index e98e9e456..000000000 --- a/Libraries/RCTWebSocketDebugger/SRWebSocket.m +++ /dev/null @@ -1,1779 +0,0 @@ -// -// Copyright 2012 Square Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - - -#import "SRWebSocket.h" - -#import - -#pragma clang diagnostic ignored "-Wshadow" - -//NOTE: libicucore ins't actually needed for the socket to function -//and by commenting this out, we avoid the need to import it into every app. - -//#if TARGET_OS_IPHONE -//#define HAS_ICU -//#endif - -#ifdef HAS_ICU -#import -#endif - -#if TARGET_OS_IPHONE -#import -#else -#import -#endif - -#import -#import - -#if OS_OBJECT_USE_OBJC_RETAIN_RELEASE -#define sr_dispatch_retain(x) -#define sr_dispatch_release(x) -#define maybe_bridge(x) ((__bridge void *) x) -#else -#define sr_dispatch_retain(x) dispatch_retain(x) -#define sr_dispatch_release(x) dispatch_release(x) -#define maybe_bridge(x) (x) -#endif - -#if !__has_feature(objc_arc) -#error SocketRocket must be compiled with ARC enabled -#endif - - -typedef enum { - SROpCodeTextFrame = 0x1, - SROpCodeBinaryFrame = 0x2, - // 3-7 reserved. - SROpCodeConnectionClose = 0x8, - SROpCodePing = 0x9, - SROpCodePong = 0xA, - // B-F reserved. -} SROpCode; - -typedef struct { - BOOL fin; -// BOOL rsv1; -// BOOL rsv2; -// BOOL rsv3; - uint8_t opcode; - BOOL masked; - uint64_t payload_length; -} frame_header; - -static NSString *const SRWebSocketAppendToSecKeyString = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; - -static inline int32_t validate_dispatch_data_partial_string(NSData *data); -static inline void SRFastLog(NSString *format, ...); - -@interface NSData (SRWebSocket) - -- (NSString *)stringBySHA1ThenBase64Encoding; - -@end - - -@interface NSString (SRWebSocket) - -- (NSString *)stringBySHA1ThenBase64Encoding; - -@end - - -@interface NSURL (SRWebSocket) - -// The origin isn't really applicable for a native application. -// So instead, just map ws -> http and wss -> https. -- (NSString *)SR_origin; - -@end - - -@interface _SRRunLoopThread : NSThread - -@property (nonatomic, readonly) NSRunLoop *runLoop; - -@end - - -static NSString *newSHA1String(const char *bytes, size_t length) { - uint8_t md[CC_SHA1_DIGEST_LENGTH]; - - assert(length >= 0); - assert(length <= UINT32_MAX); - CC_SHA1(bytes, (CC_LONG)length, md); - - NSData *data = [NSData dataWithBytes:md length:CC_SHA1_DIGEST_LENGTH]; - -#if (__IPHONE_OS_VERSION_MIN_REQUIRED && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0) \ - || (__MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_9) - - if (![NSData instancesRespondToSelector:@selector(base64EncodedStringWithOptions:)]) { - return [data base64Encoding]; - } - -#endif - - return [data base64EncodedStringWithOptions:0]; -} - -@implementation NSData (SRWebSocket) - -- (NSString *)stringBySHA1ThenBase64Encoding; -{ - return newSHA1String(self.bytes, self.length); -} - -@end - - -@implementation NSString (SRWebSocket) - -- (NSString *)stringBySHA1ThenBase64Encoding; -{ - return newSHA1String(self.UTF8String, self.length); -} - -@end - -NSString *const SRWebSocketErrorDomain = @"SRWebSocketErrorDomain"; -NSString *const SRHTTPResponseErrorKey = @"HTTPResponseStatusCode"; - -// Returns number of bytes consumed. Returning 0 means you didn't match. -// Sends bytes to callback handler; -typedef size_t (^stream_scanner)(NSData *collected_data); - -typedef void (^data_callback)(SRWebSocket *webSocket, NSData *data); - -@interface SRIOConsumer : NSObject { - stream_scanner _scanner; - data_callback _handler; - size_t _bytesNeeded; - BOOL _readToCurrentFrame; - BOOL _unmaskBytes; -} -@property (nonatomic, copy, readonly) stream_scanner consumer; -@property (nonatomic, copy, readonly) data_callback handler; -@property (nonatomic, assign) size_t bytesNeeded; -@property (nonatomic, assign, readonly) BOOL readToCurrentFrame; -@property (nonatomic, assign, readonly) BOOL unmaskBytes; - -@end - -// This class is not thread-safe, and is expected to always be run on the same queue. -@interface SRIOConsumerPool : NSObject - -- (id)initWithBufferCapacity:(NSUInteger)poolSize; - -- (SRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; -- (void)returnConsumer:(SRIOConsumer *)consumer; - -@end - -@interface SRWebSocket () - -- (void)_writeData:(NSData *)data; -- (void)_closeWithProtocolError:(NSString *)message; -- (void)_failWithError:(NSError *)error; - -- (void)_disconnect; - -- (void)_readFrameNew; -- (void)_readFrameContinue; - -- (void)_pumpScanner; - -- (void)_pumpWriting; - -- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback; -- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; -- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength; -- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler; -- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler; - -- (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data; - -- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; -- (void)_SR_commonInit; - -- (void)_initializeStreams; -- (void)_connect; - -@property (nonatomic) SRReadyState readyState; - -@property (nonatomic) NSOperationQueue *delegateOperationQueue; -@property (nonatomic) dispatch_queue_t delegateDispatchQueue; - -@end - - -@implementation SRWebSocket { - NSInteger _webSocketVersion; - - NSOperationQueue *_delegateOperationQueue; - dispatch_queue_t _delegateDispatchQueue; - - dispatch_queue_t _workQueue; - NSMutableArray *_consumers; - - NSInputStream *_inputStream; - NSOutputStream *_outputStream; - - NSMutableData *_readBuffer; - NSUInteger _readBufferOffset; - - NSMutableData *_outputBuffer; - NSUInteger _outputBufferOffset; - - uint8_t _currentFrameOpcode; - size_t _currentFrameCount; - size_t _readOpCount; - uint32_t _currentStringScanPosition; - NSMutableData *_currentFrameData; - - NSString *_closeReason; - - NSString *_secKey; - - BOOL _pinnedCertFound; - - uint8_t _currentReadMaskKey[4]; - size_t _currentReadMaskOffset; - - BOOL _consumerStopped; - - BOOL _closeWhenFinishedWriting; - BOOL _failed; - - BOOL _secure; - NSURLRequest *_urlRequest; - - CFHTTPMessageRef _receivedHTTPHeaders; - - BOOL _sentClose; - BOOL _didFail; - int _closeCode; - - BOOL _isPumping; - - NSMutableSet *_scheduledRunloops; - - // We use this to retain ourselves. - __strong SRWebSocket *_selfRetain; - - NSArray *_requestedProtocols; - SRIOConsumerPool *_consumerPool; -} - -@synthesize delegate = _delegate; -@synthesize url = _url; -@synthesize readyState = _readyState; -@synthesize protocol = _protocol; - -static __strong NSData *CRLFCRLF; - -+ (void)initialize; -{ - CRLFCRLF = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4]; -} - -- (id)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; -{ - self = [super init]; - if (self) { - assert(request.URL); - _url = request.URL; - _urlRequest = request; - - _requestedProtocols = [protocols copy]; - - [self _SR_commonInit]; - } - - return self; -} - -- (id)initWithURLRequest:(NSURLRequest *)request; -{ - return [self initWithURLRequest:request protocols:nil]; -} - -- (id)initWithURL:(NSURL *)url; -{ - return [self initWithURL:url protocols:nil]; -} - -- (id)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; -{ - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; - return [self initWithURLRequest:request protocols:protocols]; -} - -- (void)_SR_commonInit; -{ - NSString *scheme = _url.scheme.lowercaseString; - assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]); - - if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) { - _secure = YES; - } - - _readyState = SR_CONNECTING; - _consumerStopped = YES; - _webSocketVersion = 13; - - _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); - - // Going to set a specific on the queue so we can validate we're on the work queue - dispatch_queue_set_specific(_workQueue, (__bridge void *)self, maybe_bridge(_workQueue), NULL); - - _delegateDispatchQueue = dispatch_get_main_queue(); - sr_dispatch_retain(_delegateDispatchQueue); - - _readBuffer = [[NSMutableData alloc] init]; - _outputBuffer = [[NSMutableData alloc] init]; - - _currentFrameData = [[NSMutableData alloc] init]; - - _consumers = [[NSMutableArray alloc] init]; - - _consumerPool = [[SRIOConsumerPool alloc] init]; - - _scheduledRunloops = [[NSMutableSet alloc] init]; - - [self _initializeStreams]; - - // default handlers -} - -- (void)assertOnWorkQueue; -{ - assert(dispatch_get_specific((__bridge void *)self) == maybe_bridge(_workQueue)); -} - -- (void)dealloc -{ - _inputStream.delegate = nil; - _outputStream.delegate = nil; - - [_inputStream close]; - [_outputStream close]; - - sr_dispatch_release(_workQueue); - _workQueue = NULL; - - if (_receivedHTTPHeaders) { - CFRelease(_receivedHTTPHeaders); - _receivedHTTPHeaders = NULL; - } - - if (_delegateDispatchQueue) { - sr_dispatch_release(_delegateDispatchQueue); - _delegateDispatchQueue = NULL; - } -} - -#ifndef NDEBUG - -- (void)setReadyState:(SRReadyState)aReadyState; -{ - [self willChangeValueForKey:@"readyState"]; - assert(aReadyState > _readyState); - _readyState = aReadyState; - [self didChangeValueForKey:@"readyState"]; -} - -#endif - -- (void)open; -{ - assert(_url); - NSAssert(_readyState == SR_CONNECTING, @"Cannot call -(void)open on SRWebSocket more than once"); - - _selfRetain = self; - - [self _connect]; -} - -// Calls block on delegate queue -- (void)_performDelegateBlock:(dispatch_block_t)block; -{ - if (_delegateOperationQueue) { - [_delegateOperationQueue addOperationWithBlock:block]; - } else { - assert(_delegateDispatchQueue); - dispatch_async(_delegateDispatchQueue, block); - } -} - -- (void)setDelegateDispatchQueue:(dispatch_queue_t)queue; -{ - if (queue) { - sr_dispatch_retain(queue); - } - - if (_delegateDispatchQueue) { - sr_dispatch_release(_delegateDispatchQueue); - } - - _delegateDispatchQueue = queue; -} - -- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; -{ - NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept"))); - - if (acceptHeader == nil) { - return NO; - } - - NSString *concattedString = [_secKey stringByAppendingString:SRWebSocketAppendToSecKeyString]; - NSString *expectedAccept = [concattedString stringBySHA1ThenBase64Encoding]; - - return [acceptHeader isEqualToString:expectedAccept]; -} - -- (void)_HTTPHeadersDidFinish; -{ - NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders); - - if (responseCode >= 400) { - SRFastLog(@"Request failed with response code %d", responseCode); - [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2132 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"received bad response code from server %ld", (long)responseCode], SRHTTPResponseErrorKey:@(responseCode)}]]; - return; - } - - if(![self _checkHandshake:_receivedHTTPHeaders]) { - [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid Sec-WebSocket-Accept response"] forKey:NSLocalizedDescriptionKey]]]; - return; - } - - NSString *negotiatedProtocol = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(_receivedHTTPHeaders, CFSTR("Sec-WebSocket-Protocol"))); - if (negotiatedProtocol) { - // Make sure we requested the protocol - if ([_requestedProtocols indexOfObject:negotiatedProtocol] == NSNotFound) { - [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2133 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Server specified Sec-WebSocket-Protocol that wasn't requested"] forKey:NSLocalizedDescriptionKey]]]; - return; - } - - _protocol = negotiatedProtocol; - } - - self.readyState = SR_OPEN; - - if (!_didFail) { - [self _readFrameNew]; - } - - [self _performDelegateBlock:^{ - if ([self.delegate respondsToSelector:@selector(webSocketDidOpen:)]) { - [self.delegate webSocketDidOpen:self]; - }; - }]; -} - - -- (void)_readHTTPHeader; -{ - if (_receivedHTTPHeaders == NULL) { - _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO); - } - - [self _readUntilHeaderCompleteWithCallback:^(SRWebSocket *self, NSData *data) { - CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length); - - if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) { - SRFastLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders))); - [self _HTTPHeadersDidFinish]; - } else { - [self _readHTTPHeader]; - } - }]; -} - -- (void)didConnect -{ - SRFastLog(@"Connected"); - CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1); - - // Set host first so it defaults - CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host)); - - NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16]; - SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes); - -#if (__IPHONE_OS_VERSION_MIN_REQUIRED && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_7_0) \ - || (__MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_9) - - if (![NSData instancesRespondToSelector:@selector(base64EncodedStringWithOptions:)]) { - _secKey = [keyBytes base64Encoding]; - } else - -#endif - - { - _secKey = [keyBytes base64EncodedStringWithOptions:0]; - } - - assert([_secKey length] == 24); - - CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Upgrade"), CFSTR("websocket")); - CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Upgrade")); - CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)_secKey); - CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", (long)_webSocketVersion]); - - CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Origin"), (__bridge CFStringRef)_url.SR_origin); - - if (_requestedProtocols) { - CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Protocol"), (__bridge CFStringRef)[_requestedProtocols componentsJoinedByString:@", "]); - } - - [_urlRequest.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { - CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj); - }]; - - NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request)); - - CFRelease(request); - - [self _writeData:message]; - [self _readHTTPHeader]; -} - -- (void)_initializeStreams; -{ - assert(_url.port.unsignedIntValue <= UINT32_MAX); - uint32_t port = _url.port.unsignedIntValue; - if (port == 0) { - if (!_secure) { - port = 80; - } else { - port = 443; - } - } - NSString *host = _url.host; - - CFReadStreamRef readStream = NULL; - CFWriteStreamRef writeStream = NULL; - - CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream); - - _outputStream = CFBridgingRelease(writeStream); - _inputStream = CFBridgingRelease(readStream); - - - if (_secure) { - NSMutableDictionary *SSLOptions = [[NSMutableDictionary alloc] init]; - - [_outputStream setProperty:(__bridge id)kCFStreamSocketSecurityLevelNegotiatedSSL forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel]; - - // If we're using pinned certs, don't validate the certificate chain - if ([_urlRequest SR_SSLPinnedCertificates].count) { - [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; - } - -#if DEBUG - [SSLOptions setValue:[NSNumber numberWithBool:NO] forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; - NSLog(@"SocketRocket: In debug mode. Allowing connection to any root cert"); -#endif - - [_outputStream setProperty:SSLOptions - forKey:(__bridge id)kCFStreamPropertySSLSettings]; - } - - _inputStream.delegate = self; - _outputStream.delegate = self; -} - -- (void)_connect; -{ - if (!_scheduledRunloops.count) { - [self scheduleInRunLoop:[NSRunLoop SR_networkRunLoop] forMode:NSDefaultRunLoopMode]; - } - - - [_outputStream open]; - [_inputStream open]; -} - -- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; -{ - [_outputStream scheduleInRunLoop:aRunLoop forMode:mode]; - [_inputStream scheduleInRunLoop:aRunLoop forMode:mode]; - - [_scheduledRunloops addObject:@[aRunLoop, mode]]; -} - -- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; -{ - [_outputStream removeFromRunLoop:aRunLoop forMode:mode]; - [_inputStream removeFromRunLoop:aRunLoop forMode:mode]; - - [_scheduledRunloops removeObject:@[aRunLoop, mode]]; -} - -- (void)close; -{ - [self closeWithCode:SRStatusCodeNormal reason:nil]; -} - -- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason; -{ - assert(code); - dispatch_async(_workQueue, ^{ - if (self.readyState == SR_CLOSING || self.readyState == SR_CLOSED) { - return; - } - - BOOL wasConnecting = self.readyState == SR_CONNECTING; - - self.readyState = SR_CLOSING; - - SRFastLog(@"Closing with code %d reason %@", code, reason); - - if (wasConnecting) { - [self _disconnect]; - return; - } - - size_t maxMsgSize = [reason maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding]; - NSMutableData *mutablePayload = [[NSMutableData alloc] initWithLength:sizeof(uint16_t) + maxMsgSize]; - NSData *payload = mutablePayload; - - ((uint16_t *)mutablePayload.mutableBytes)[0] = EndianU16_BtoN(code); - - if (reason) { - NSRange remainingRange = {0}; - - NSUInteger usedLength = 0; - - BOOL success = [reason getBytes:(char *)mutablePayload.mutableBytes + sizeof(uint16_t) maxLength:payload.length - sizeof(uint16_t) usedLength:&usedLength encoding:NSUTF8StringEncoding options:NSStringEncodingConversionExternalRepresentation range:NSMakeRange(0, reason.length) remainingRange:&remainingRange]; - - assert(success); - assert(remainingRange.length == 0); - - if (usedLength != maxMsgSize) { - payload = [payload subdataWithRange:NSMakeRange(0, usedLength + sizeof(uint16_t))]; - } - } - - - [self _sendFrameWithOpcode:SROpCodeConnectionClose data:payload]; - }); -} - -- (void)_closeWithProtocolError:(NSString *)message; -{ - // Need to shunt this on the _callbackQueue first to see if they received any messages - [self _performDelegateBlock:^{ - [self closeWithCode:SRStatusCodeProtocolError reason:message]; - dispatch_async(_workQueue, ^{ - [self _disconnect]; - }); - }]; -} - -- (void)_failWithError:(NSError *)error; -{ - dispatch_async(_workQueue, ^{ - if (self.readyState != SR_CLOSED) { - _failed = YES; - [self _performDelegateBlock:^{ - if ([self.delegate respondsToSelector:@selector(webSocket:didFailWithError:)]) { - [self.delegate webSocket:self didFailWithError:error]; - } - }]; - - self.readyState = SR_CLOSED; - _selfRetain = nil; - - SRFastLog(@"Failing with error %@", error.localizedDescription); - - [self _disconnect]; - } - }); -} - -- (void)_writeData:(NSData *)data; -{ - [self assertOnWorkQueue]; - - if (_closeWhenFinishedWriting) { - return; - } - [_outputBuffer appendData:data]; - [self _pumpWriting]; -} - -- (void)send:(id)data; -{ - NSAssert(self.readyState != SR_CONNECTING, @"Invalid State: Cannot call send: until connection is open"); - // TODO: maybe not copy this for performance - data = [data copy]; - dispatch_async(_workQueue, ^{ - if ([data isKindOfClass:[NSString class]]) { - [self _sendFrameWithOpcode:SROpCodeTextFrame data:[(NSString *)data dataUsingEncoding:NSUTF8StringEncoding]]; - } else if ([data isKindOfClass:[NSData class]]) { - [self _sendFrameWithOpcode:SROpCodeBinaryFrame data:data]; - } else if (data == nil) { - [self _sendFrameWithOpcode:SROpCodeTextFrame data:data]; - } else { - assert(NO); - } - }); -} - -- (void)sendPing:(NSData *)data; -{ - NSAssert(self.readyState == SR_OPEN, @"Invalid State: Cannot call send: until connection is open"); - // TODO: maybe not copy this for performance - data = [data copy] ?: [NSData data]; // It's okay for a ping to be empty - dispatch_async(_workQueue, ^{ - [self _sendFrameWithOpcode:SROpCodePing data:data]; - }); -} - -- (void)handlePing:(NSData *)pingData; -{ - // Need to pingpong this off _callbackQueue first to make sure messages happen in order - [self _performDelegateBlock:^{ - dispatch_async(_workQueue, ^{ - [self _sendFrameWithOpcode:SROpCodePong data:pingData]; - }); - }]; -} - -- (void)handlePong:(NSData *)pongData; -{ - SRFastLog(@"Received pong"); - [self _performDelegateBlock:^{ - if ([self.delegate respondsToSelector:@selector(webSocket:didReceivePong:)]) { - [self.delegate webSocket:self didReceivePong:pongData]; - } - }]; -} - -- (void)_handleMessage:(id)message -{ - SRFastLog(@"Received message"); - [self _performDelegateBlock:^{ - [self.delegate webSocket:self didReceiveMessage:message]; - }]; -} - - -static inline BOOL closeCodeIsValid(int closeCode) { - if (closeCode < 1000) { - return NO; - } - - if (closeCode >= 1000 && closeCode <= 1011) { - if (closeCode == 1004 || - closeCode == 1005 || - closeCode == 1006) { - return NO; - } - return YES; - } - - if (closeCode >= 3000 && closeCode <= 3999) { - return YES; - } - - if (closeCode >= 4000 && closeCode <= 4999) { - return YES; - } - - return NO; -} - -// Note from RFC: -// -// If there is a body, the first two -// bytes of the body MUST be a 2-byte unsigned integer (in network byte -// order) representing a status code with value /code/ defined in -// Section 7.4. Following the 2-byte integer the body MAY contain UTF-8 -// encoded data with value /reason/, the interpretation of which is not -// defined by this specification. - -- (void)handleCloseWithData:(NSData *)data; -{ - size_t dataSize = data.length; - __block uint16_t closeCode = 0; - - SRFastLog(@"Received close frame"); - - if (dataSize == 1) { - // TODO handle error - [self _closeWithProtocolError:@"Payload for close must be larger than 2 bytes"]; - return; - } else if (dataSize >= 2) { - [data getBytes:&closeCode length:sizeof(closeCode)]; - _closeCode = EndianU16_BtoN(closeCode); - if (!closeCodeIsValid(_closeCode)) { - [self _closeWithProtocolError:[NSString stringWithFormat:@"Cannot have close code of %d", _closeCode]]; - return; - } - if (dataSize > 2) { - _closeReason = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(2, dataSize - 2)] encoding:NSUTF8StringEncoding]; - if (!_closeReason) { - [self _closeWithProtocolError:@"Close reason MUST be valid UTF-8"]; - return; - } - } - } else { - _closeCode = SRStatusNoStatusReceived; - } - - [self assertOnWorkQueue]; - - if (self.readyState == SR_OPEN) { - [self closeWithCode:1000 reason:nil]; - } - dispatch_async(_workQueue, ^{ - [self _disconnect]; - }); -} - -- (void)_disconnect; -{ - [self assertOnWorkQueue]; - SRFastLog(@"Trying to disconnect"); - _closeWhenFinishedWriting = YES; - [self _pumpWriting]; -} - -- (void)_handleFrameWithData:(NSData *)frameData opCode:(NSInteger)opcode; -{ - // Check that the current data is valid UTF8 - - BOOL isControlFrame = (opcode == SROpCodePing || opcode == SROpCodePong || opcode == SROpCodeConnectionClose); - if (!isControlFrame) { - [self _readFrameNew]; - } else { - dispatch_async(_workQueue, ^{ - [self _readFrameContinue]; - }); - } - - switch (opcode) { - case SROpCodeTextFrame: { - NSString *str = [[NSString alloc] initWithData:frameData encoding:NSUTF8StringEncoding]; - if (str == nil && frameData) { - [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"]; - dispatch_async(_workQueue, ^{ - [self _disconnect]; - }); - - return; - } - [self _handleMessage:str]; - break; - } - case SROpCodeBinaryFrame: - [self _handleMessage:[frameData copy]]; - break; - case SROpCodeConnectionClose: - [self handleCloseWithData:frameData]; - break; - case SROpCodePing: - [self handlePing:frameData]; - break; - case SROpCodePong: - [self handlePong:frameData]; - break; - default: - [self _closeWithProtocolError:[NSString stringWithFormat:@"Unknown opcode %ld", (long)opcode]]; - // TODO: Handle invalid opcode - break; - } -} - -- (void)_handleFrameHeader:(frame_header)frame_header curData:(NSData *)curData; -{ - assert(frame_header.opcode != 0); - - if (self.readyState != SR_OPEN) { - return; - } - - - BOOL isControlFrame = (frame_header.opcode == SROpCodePing || frame_header.opcode == SROpCodePong || frame_header.opcode == SROpCodeConnectionClose); - - if (isControlFrame && !frame_header.fin) { - [self _closeWithProtocolError:@"Fragmented control frames not allowed"]; - return; - } - - if (isControlFrame && frame_header.payload_length >= 126) { - [self _closeWithProtocolError:@"Control frames cannot have payloads larger than 126 bytes"]; - return; - } - - if (!isControlFrame) { - _currentFrameOpcode = frame_header.opcode; - _currentFrameCount += 1; - } - - if (frame_header.payload_length == 0) { - if (isControlFrame) { - [self _handleFrameWithData:curData opCode:frame_header.opcode]; - } else { - if (frame_header.fin) { - [self _handleFrameWithData:_currentFrameData opCode:frame_header.opcode]; - } else { - // TODO add assert that opcode is not a control; - [self _readFrameContinue]; - } - } - } else { - assert(frame_header.payload_length <= SIZE_T_MAX); - [self _addConsumerWithDataLength:(size_t)frame_header.payload_length callback:^(SRWebSocket *self, NSData *newData) { - if (isControlFrame) { - [self _handleFrameWithData:newData opCode:frame_header.opcode]; - } else { - if (frame_header.fin) { - [self _handleFrameWithData:self->_currentFrameData opCode:frame_header.opcode]; - } else { - // TODO add assert that opcode is not a control; - [self _readFrameContinue]; - } - - } - } readToCurrentFrame:!isControlFrame unmaskBytes:frame_header.masked]; - } -} - -/* From RFC: - - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-------+-+-------------+-------------------------------+ - |F|R|R|R| opcode|M| Payload len | Extended payload length | - |I|S|S|S| (4) |A| (7) | (16/64) | - |N|V|V|V| |S| | (if payload len==126/127) | - | |1|2|3| |K| | | - +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + - | Extended payload length continued, if payload len == 127 | - + - - - - - - - - - - - - - - - +-------------------------------+ - | |Masking-key, if MASK set to 1 | - +-------------------------------+-------------------------------+ - | Masking-key (continued) | Payload Data | - +-------------------------------- - - - - - - - - - - - - - - - + - : Payload Data continued ... : - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - | Payload Data continued ... | - +---------------------------------------------------------------+ - */ - -static const uint8_t SRFinMask = 0x80; -static const uint8_t SROpCodeMask = 0x0F; -static const uint8_t SRRsvMask = 0x70; -static const uint8_t SRMaskMask = 0x80; -static const uint8_t SRPayloadLenMask = 0x7F; - - -- (void)_readFrameContinue; -{ - assert((_currentFrameCount == 0 && _currentFrameOpcode == 0) || (_currentFrameCount > 0 && _currentFrameOpcode > 0)); - - [self _addConsumerWithDataLength:2 callback:^(SRWebSocket *self, NSData *data) { - __block frame_header header = {0}; - - const uint8_t *headerBuffer = data.bytes; - assert(data.length >= 2); - - if (headerBuffer[0] & SRRsvMask) { - [self _closeWithProtocolError:@"Server used RSV bits"]; - return; - } - - uint8_t receivedOpcode = (SROpCodeMask & headerBuffer[0]); - - BOOL isControlFrame = (receivedOpcode == SROpCodePing || receivedOpcode == SROpCodePong || receivedOpcode == SROpCodeConnectionClose); - - if (!isControlFrame && receivedOpcode != 0 && self->_currentFrameCount > 0) { - [self _closeWithProtocolError:@"all data frames after the initial data frame must have opcode 0"]; - return; - } - - if (receivedOpcode == 0 && self->_currentFrameCount == 0) { - [self _closeWithProtocolError:@"cannot continue a message"]; - return; - } - - header.opcode = receivedOpcode == 0 ? self->_currentFrameOpcode : receivedOpcode; - - header.fin = !!(SRFinMask & headerBuffer[0]); - - - header.masked = !!(SRMaskMask & headerBuffer[1]); - header.payload_length = SRPayloadLenMask & headerBuffer[1]; - - headerBuffer = NULL; - - if (header.masked) { - [self _closeWithProtocolError:@"Client must receive unmasked data"]; - } - - size_t extra_bytes_needed = header.masked ? sizeof(_currentReadMaskKey) : 0; - - if (header.payload_length == 126) { - extra_bytes_needed += sizeof(uint16_t); - } else if (header.payload_length == 127) { - extra_bytes_needed += sizeof(uint64_t); - } - - if (extra_bytes_needed == 0) { - [self _handleFrameHeader:header curData:self->_currentFrameData]; - } else { - [self _addConsumerWithDataLength:extra_bytes_needed callback:^(SRWebSocket *self, NSData *data) { - size_t mapped_size = data.length; - const void *mapped_buffer = data.bytes; - size_t offset = 0; - - if (header.payload_length == 126) { - assert(mapped_size >= sizeof(uint16_t)); - uint16_t newLen = EndianU16_BtoN(*(uint16_t *)(mapped_buffer)); - header.payload_length = newLen; - offset += sizeof(uint16_t); - } else if (header.payload_length == 127) { - assert(mapped_size >= sizeof(uint64_t)); - header.payload_length = EndianU64_BtoN(*(uint64_t *)(mapped_buffer)); - offset += sizeof(uint64_t); - } else { - assert(header.payload_length < 126 && header.payload_length >= 0); - } - - - if (header.masked) { - assert(mapped_size >= sizeof(_currentReadMaskOffset) + offset); - memcpy(self->_currentReadMaskKey, ((uint8_t *)mapped_buffer) + offset, sizeof(self->_currentReadMaskKey)); - } - - [self _handleFrameHeader:header curData:self->_currentFrameData]; - } readToCurrentFrame:NO unmaskBytes:NO]; - } - } readToCurrentFrame:NO unmaskBytes:NO]; -} - -- (void)_readFrameNew; -{ - dispatch_async(_workQueue, ^{ - [_currentFrameData setLength:0]; - - _currentFrameOpcode = 0; - _currentFrameCount = 0; - _readOpCount = 0; - _currentStringScanPosition = 0; - - [self _readFrameContinue]; - }); -} - -- (void)_pumpWriting; -{ - [self assertOnWorkQueue]; - - NSUInteger dataLength = _outputBuffer.length; - if (dataLength - _outputBufferOffset > 0 && _outputStream.hasSpaceAvailable) { - NSInteger bytesWritten = [_outputStream write:_outputBuffer.bytes + _outputBufferOffset maxLength:dataLength - _outputBufferOffset]; - if (bytesWritten == -1) { - [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:2145 userInfo:[NSDictionary dictionaryWithObject:@"Error writing to stream" forKey:NSLocalizedDescriptionKey]]]; - return; - } - - _outputBufferOffset += bytesWritten; - - if (_outputBufferOffset > 4096 && _outputBufferOffset > (_outputBuffer.length >> 1)) { - _outputBuffer = [[NSMutableData alloc] initWithBytes:(char *)_outputBuffer.bytes + _outputBufferOffset length:_outputBuffer.length - _outputBufferOffset]; - _outputBufferOffset = 0; - } - } - - if (_closeWhenFinishedWriting && - _outputBuffer.length - _outputBufferOffset == 0 && - (_inputStream.streamStatus != NSStreamStatusNotOpen && - _inputStream.streamStatus != NSStreamStatusClosed) && - !_sentClose) { - _sentClose = YES; - - [_outputStream close]; - [_inputStream close]; - - - for (NSArray *runLoop in [_scheduledRunloops copy]) { - [self unscheduleFromRunLoop:[runLoop objectAtIndex:0] forMode:[runLoop objectAtIndex:1]]; - } - - if (!_failed) { - [self _performDelegateBlock:^{ - if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { - [self.delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES]; - } - }]; - } - - _selfRetain = nil; - } -} - -- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback; -{ - [self assertOnWorkQueue]; - [self _addConsumerWithScanner:consumer callback:callback dataLength:0]; -} - -- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; -{ - [self assertOnWorkQueue]; - assert(dataLength); - - [_consumers addObject:[_consumerPool consumerWithScanner:nil handler:callback bytesNeeded:dataLength readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]]; - [self _pumpScanner]; -} - -- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength; -{ - [self assertOnWorkQueue]; - [_consumers addObject:[_consumerPool consumerWithScanner:consumer handler:callback bytesNeeded:dataLength readToCurrentFrame:NO unmaskBytes:NO]]; - [self _pumpScanner]; -} - - -static const char CRLFCRLFBytes[] = {'\r', '\n', '\r', '\n'}; - -- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler; -{ - [self _readUntilBytes:CRLFCRLFBytes length:sizeof(CRLFCRLFBytes) callback:dataHandler]; -} - -- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler; -{ - // TODO optimize so this can continue from where we last searched - stream_scanner consumer = ^size_t(NSData *data) { - __block size_t found_size = 0; - __block size_t match_count = 0; - - size_t size = data.length; - const unsigned char *buffer = data.bytes; - for (size_t i = 0; i < size; i++ ) { - if (((const unsigned char *)buffer)[i] == ((const unsigned char *)bytes)[match_count]) { - match_count += 1; - if (match_count == length) { - found_size = i + 1; - break; - } - } else { - match_count = 0; - } - } - return found_size; - }; - [self _addConsumerWithScanner:consumer callback:dataHandler]; -} - - -// Returns true if did work -- (BOOL)_innerPumpScanner { - - BOOL didWork = NO; - - if (self.readyState >= SR_CLOSING) { - return didWork; - } - - if (!_consumers.count) { - return didWork; - } - - size_t curSize = _readBuffer.length - _readBufferOffset; - if (!curSize) { - return didWork; - } - - SRIOConsumer *consumer = [_consumers objectAtIndex:0]; - - size_t bytesNeeded = consumer.bytesNeeded; - - size_t foundSize = 0; - if (consumer.consumer) { - NSData *tempView = [NSData dataWithBytesNoCopy:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset freeWhenDone:NO]; - foundSize = consumer.consumer(tempView); - } else { - assert(consumer.bytesNeeded); - if (curSize >= bytesNeeded) { - foundSize = bytesNeeded; - } else if (consumer.readToCurrentFrame) { - foundSize = curSize; - } - } - - NSData *slice = nil; - if (consumer.readToCurrentFrame || foundSize) { - NSRange sliceRange = NSMakeRange(_readBufferOffset, foundSize); - slice = [_readBuffer subdataWithRange:sliceRange]; - - _readBufferOffset += foundSize; - - if (_readBufferOffset > 4096 && _readBufferOffset > (_readBuffer.length >> 1)) { - _readBuffer = [[NSMutableData alloc] initWithBytes:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset]; _readBufferOffset = 0; - } - - if (consumer.unmaskBytes) { - NSMutableData *mutableSlice = [slice mutableCopy]; - - NSUInteger len = mutableSlice.length; - uint8_t *bytes = mutableSlice.mutableBytes; - - for (NSUInteger i = 0; i < len; i++) { - bytes[i] = bytes[i] ^ _currentReadMaskKey[_currentReadMaskOffset % sizeof(_currentReadMaskKey)]; - _currentReadMaskOffset += 1; - } - - slice = mutableSlice; - } - - if (consumer.readToCurrentFrame) { - [_currentFrameData appendData:slice]; - - _readOpCount += 1; - - if (_currentFrameOpcode == SROpCodeTextFrame) { - // Validate UTF8 stuff. - size_t currentDataSize = _currentFrameData.length; - if (_currentFrameOpcode == SROpCodeTextFrame && currentDataSize > 0) { - // TODO: Optimize the crap out of this. Don't really have to copy all the data each time - - size_t scanSize = currentDataSize - _currentStringScanPosition; - - NSData *scan_data = [_currentFrameData subdataWithRange:NSMakeRange(_currentStringScanPosition, scanSize)]; - int32_t valid_utf8_size = validate_dispatch_data_partial_string(scan_data); - - if (valid_utf8_size == -1) { - [self closeWithCode:SRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"]; - dispatch_async(_workQueue, ^{ - [self _disconnect]; - }); - return didWork; - } else { - _currentStringScanPosition += valid_utf8_size; - } - } - - } - - consumer.bytesNeeded -= foundSize; - - if (consumer.bytesNeeded == 0) { - [_consumers removeObjectAtIndex:0]; - consumer.handler(self, nil); - [_consumerPool returnConsumer:consumer]; - didWork = YES; - } - } else if (foundSize) { - [_consumers removeObjectAtIndex:0]; - consumer.handler(self, slice); - [_consumerPool returnConsumer:consumer]; - didWork = YES; - } - } - return didWork; -} - --(void)_pumpScanner; -{ - [self assertOnWorkQueue]; - - if (!_isPumping) { - _isPumping = YES; - } else { - return; - } - - while ([self _innerPumpScanner]) { - - } - - _isPumping = NO; -} - -//#define NOMASK - -static const size_t SRFrameHeaderOverhead = 32; - -- (void)_sendFrameWithOpcode:(SROpCode)opcode data:(id)data; -{ - [self assertOnWorkQueue]; - - if (nil == data) { - return; - } - - NSAssert([data isKindOfClass:[NSData class]] || [data isKindOfClass:[NSString class]], @"NSString or NSData"); - - size_t payloadLength = [data isKindOfClass:[NSString class]] ? [(NSString *)data lengthOfBytesUsingEncoding:NSUTF8StringEncoding] : [data length]; - - NSMutableData *frame = [[NSMutableData alloc] initWithLength:payloadLength + SRFrameHeaderOverhead]; - if (!frame) { - [self closeWithCode:SRStatusCodeMessageTooBig reason:@"Message too big"]; - return; - } - uint8_t *frame_buffer = (uint8_t *)[frame mutableBytes]; - - // set fin - frame_buffer[0] = SRFinMask | opcode; - - BOOL useMask = YES; -#ifdef NOMASK - useMask = NO; -#endif - - if (useMask) { - // set the mask and header - frame_buffer[1] |= SRMaskMask; - } - - size_t frame_buffer_size = 2; - - const uint8_t *unmasked_payload = NULL; - if ([data isKindOfClass:[NSData class]]) { - unmasked_payload = (uint8_t *)[data bytes]; - } else if ([data isKindOfClass:[NSString class]]) { - unmasked_payload = (const uint8_t *)[data UTF8String]; - } else { - return; - } - - if (payloadLength < 126) { - frame_buffer[1] |= payloadLength; - } else if (payloadLength <= UINT16_MAX) { - frame_buffer[1] |= 126; - *((uint16_t *)(frame_buffer + frame_buffer_size)) = EndianU16_BtoN((uint16_t)payloadLength); - frame_buffer_size += sizeof(uint16_t); - } else { - frame_buffer[1] |= 127; - *((uint64_t *)(frame_buffer + frame_buffer_size)) = EndianU64_BtoN((uint64_t)payloadLength); - frame_buffer_size += sizeof(uint64_t); - } - - if (!useMask) { - for (size_t i = 0; i < payloadLength; i++) { - frame_buffer[frame_buffer_size] = unmasked_payload[i]; - frame_buffer_size += 1; - } - } else { - uint8_t *mask_key = frame_buffer + frame_buffer_size; - SecRandomCopyBytes(kSecRandomDefault, sizeof(uint32_t), (uint8_t *)mask_key); - frame_buffer_size += sizeof(uint32_t); - - // TODO: could probably optimize this with SIMD - for (size_t i = 0; i < payloadLength; i++) { - frame_buffer[frame_buffer_size] = unmasked_payload[i] ^ mask_key[i % sizeof(uint32_t)]; - frame_buffer_size += 1; - } - } - - assert(frame_buffer_size <= [frame length]); - frame.length = frame_buffer_size; - - [self _writeData:frame]; -} - -- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode; -{ - if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) { - - NSArray *sslCerts = [_urlRequest SR_SSLPinnedCertificates]; - if (sslCerts) { - SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust]; - if (secTrust) { - NSInteger numCerts = SecTrustGetCertificateCount(secTrust); - for (NSInteger i = 0; i < numCerts && !_pinnedCertFound; i++) { - SecCertificateRef cert = SecTrustGetCertificateAtIndex(secTrust, i); - NSData *certData = CFBridgingRelease(SecCertificateCopyData(cert)); - - for (id ref in sslCerts) { - SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref; - NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert)); - - if ([trustedCertData isEqualToData:certData]) { - _pinnedCertFound = YES; - break; - } - } - } - } - - if (!_pinnedCertFound) { - dispatch_async(_workQueue, ^{ - [self _failWithError:[NSError errorWithDomain:SRWebSocketErrorDomain code:23556 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Invalid server cert"] forKey:NSLocalizedDescriptionKey]]]; - }); - return; - } - } - } - - dispatch_async(_workQueue, ^{ - switch (eventCode) { - case NSStreamEventOpenCompleted: { - SRFastLog(@"NSStreamEventOpenCompleted %@", aStream); - if (self.readyState >= SR_CLOSING) { - return; - } - assert(_readBuffer); - - if (self.readyState == SR_CONNECTING && aStream == _inputStream) { - [self didConnect]; - } - [self _pumpWriting]; - [self _pumpScanner]; - break; - } - - case NSStreamEventErrorOccurred: { - SRFastLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [[aStream streamError] copy]); - /// TODO specify error better! - [self _failWithError:aStream.streamError]; - _readBufferOffset = 0; - [_readBuffer setLength:0]; - break; - - } - - case NSStreamEventEndEncountered: { - [self _pumpScanner]; - SRFastLog(@"NSStreamEventEndEncountered %@", aStream); - if (aStream.streamError) { - [self _failWithError:aStream.streamError]; - } else { - if (self.readyState != SR_CLOSED) { - self.readyState = SR_CLOSED; - _selfRetain = nil; - } - - if (!_sentClose && !_failed) { - _sentClose = YES; - // If we get closed in this state it's probably not clean because we should be sending this when we send messages - [self _performDelegateBlock:^{ - if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { - [self.delegate webSocket:self didCloseWithCode:SRStatusCodeGoingAway reason:@"Stream end encountered" wasClean:NO]; - } - }]; - } - } - - break; - } - - case NSStreamEventHasBytesAvailable: { - SRFastLog(@"NSStreamEventHasBytesAvailable %@", aStream); - const int bufferSize = 2048; - uint8_t buffer[bufferSize]; - - while (_inputStream.hasBytesAvailable) { - NSInteger bytes_read = [_inputStream read:buffer maxLength:bufferSize]; - - if (bytes_read > 0) { - [_readBuffer appendBytes:buffer length:bytes_read]; - } else if (bytes_read < 0) { - [self _failWithError:_inputStream.streamError]; - } - - if (bytes_read != bufferSize) { - break; - } - }; - [self _pumpScanner]; - break; - } - - case NSStreamEventHasSpaceAvailable: { - SRFastLog(@"NSStreamEventHasSpaceAvailable %@", aStream); - [self _pumpWriting]; - break; - } - - default: - SRFastLog(@"(default) %@", aStream); - break; - } - }); -} - -@end - - -@implementation SRIOConsumer - -@synthesize bytesNeeded = _bytesNeeded; -@synthesize consumer = _scanner; -@synthesize handler = _handler; -@synthesize readToCurrentFrame = _readToCurrentFrame; -@synthesize unmaskBytes = _unmaskBytes; - -- (void)setupWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; -{ - _scanner = [scanner copy]; - _handler = [handler copy]; - _bytesNeeded = bytesNeeded; - _readToCurrentFrame = readToCurrentFrame; - _unmaskBytes = unmaskBytes; - assert(_scanner || _bytesNeeded); -} - - -@end - - -@implementation SRIOConsumerPool { - NSUInteger _poolSize; - NSMutableArray *_bufferedConsumers; -} - -- (id)initWithBufferCapacity:(NSUInteger)poolSize; -{ - self = [super init]; - if (self) { - _poolSize = poolSize; - _bufferedConsumers = [[NSMutableArray alloc] initWithCapacity:poolSize]; - } - return self; -} - -- (id)init -{ - return [self initWithBufferCapacity:8]; -} - -- (SRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; -{ - SRIOConsumer *consumer = nil; - if (_bufferedConsumers.count) { - consumer = [_bufferedConsumers lastObject]; - [_bufferedConsumers removeLastObject]; - } else { - consumer = [[SRIOConsumer alloc] init]; - } - - [consumer setupWithScanner:scanner handler:handler bytesNeeded:bytesNeeded readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]; - - return consumer; -} - -- (void)returnConsumer:(SRIOConsumer *)consumer; -{ - if (_bufferedConsumers.count < _poolSize) { - [_bufferedConsumers addObject:consumer]; - } -} - -@end - - -@implementation NSURLRequest (CertificateAdditions) - -- (NSArray *)SR_SSLPinnedCertificates; -{ - return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self]; -} - -@end - -@implementation NSMutableURLRequest (CertificateAdditions) - -- (NSArray *)SR_SSLPinnedCertificates; -{ - return [NSURLProtocol propertyForKey:@"SR_SSLPinnedCertificates" inRequest:self]; -} - -- (void)setSR_SSLPinnedCertificates:(NSArray *)SR_SSLPinnedCertificates; -{ - [NSURLProtocol setProperty:SR_SSLPinnedCertificates forKey:@"SR_SSLPinnedCertificates" inRequest:self]; -} - -@end - -@implementation NSURL (SRWebSocket) - -- (NSString *)SR_origin; -{ - NSString *scheme = [self.scheme lowercaseString]; - - if ([scheme isEqualToString:@"wss"]) { - scheme = @"https"; - } else if ([scheme isEqualToString:@"ws"]) { - scheme = @"http"; - } - - if (self.port) { - return [NSString stringWithFormat:@"%@://%@:%@/", scheme, self.host, self.port]; - } else { - return [NSString stringWithFormat:@"%@://%@/", scheme, self.host]; - } -} - -@end - -//#define SR_ENABLE_LOG - -static inline void SRFastLog(NSString *format, ...) { -#ifdef SR_ENABLE_LOG - __block va_list arg_list; - va_start (arg_list, format); - - NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list]; - - va_end(arg_list); - - NSLog(@"[SR] %@", formattedString); -#endif -} - - -#ifdef HAS_ICU - -static inline int32_t validate_dispatch_data_partial_string(NSData *data) { - if ([data length] > INT32_MAX) { - // INT32_MAX is the limit so long as this Framework is using 32 bit ints everywhere. - return -1; - } - - int32_t size = (int32_t)[data length]; - - const void * contents = [data bytes]; - const uint8_t *str = (const uint8_t *)contents; - - UChar32 codepoint = 1; - int32_t offset = 0; - int32_t lastOffset = 0; - while(offset < size && codepoint > 0) { - lastOffset = offset; - U8_NEXT(str, offset, size, codepoint); - } - - if (codepoint == -1) { - // Check to see if the last byte is valid or whether it was just continuing - if (!U8_IS_LEAD(str[lastOffset]) || U8_COUNT_TRAIL_BYTES(str[lastOffset]) + lastOffset < (int32_t)size) { - - size = -1; - } else { - uint8_t leadByte = str[lastOffset]; - U8_MASK_LEAD_BYTE(leadByte, U8_COUNT_TRAIL_BYTES(leadByte)); - - for (int i = lastOffset + 1; i < offset; i++) { - if (U8_IS_SINGLE(str[i]) || U8_IS_LEAD(str[i]) || !U8_IS_TRAIL(str[i])) { - size = -1; - } - } - - if (size != -1) { - size = lastOffset; - } - } - } - - if (size != -1 && ![[NSString alloc] initWithBytesNoCopy:(char *)[data bytes] length:size encoding:NSUTF8StringEncoding freeWhenDone:NO]) { - size = -1; - } - - return size; -} - -#else - -// This is a hack, and probably not optimal -static inline int32_t validate_dispatch_data_partial_string(NSData *data) { - static const int maxCodepointSize = 3; - - for (int i = 0; i < maxCodepointSize; i++) { - NSString *str = [[NSString alloc] initWithBytesNoCopy:(char *)data.bytes length:data.length - i encoding:NSUTF8StringEncoding freeWhenDone:NO]; - if (str) { - return (int32_t)(data.length - i); - } - } - - return -1; -} - -#endif - -static _SRRunLoopThread *networkThread = nil; -static NSRunLoop *networkRunLoop = nil; - -@implementation NSRunLoop (SRWebSocket) - -+ (NSRunLoop *)SR_networkRunLoop { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - networkThread = [[_SRRunLoopThread alloc] init]; - networkThread.name = @"com.squareup.SocketRocket.NetworkThread"; - [networkThread start]; - networkRunLoop = networkThread.runLoop; - }); - - return networkRunLoop; -} - -@end - - -@implementation _SRRunLoopThread { - dispatch_group_t _waitGroup; -} - -@synthesize runLoop = _runLoop; - -- (void)dealloc -{ - sr_dispatch_release(_waitGroup); -} - -- (id)init -{ - self = [super init]; - if (self) { - _waitGroup = dispatch_group_create(); - dispatch_group_enter(_waitGroup); - } - return self; -} - -- (void)main; -{ - @autoreleasepool { - _runLoop = [NSRunLoop currentRunLoop]; - dispatch_group_leave(_waitGroup); - - NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate distantFuture] interval:0.0 target:nil selector:nil userInfo:nil repeats:NO]; - [_runLoop addTimer:timer forMode:NSDefaultRunLoopMode]; - - while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) { - - } - assert(NO); - } -} - -- (NSRunLoop *)runLoop; -{ - dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER); - return _runLoop; -} - -@end diff --git a/Libraries/ReactIOS/NativeMethodsMixin.js b/Libraries/ReactIOS/NativeMethodsMixin.js index 1bbf30b06..4bc99f56b 100644 --- a/Libraries/ReactIOS/NativeMethodsMixin.js +++ b/Libraries/ReactIOS/NativeMethodsMixin.js @@ -20,6 +20,7 @@ var findNodeHandle = require('findNodeHandle'); var flattenStyle = require('flattenStyle'); var invariant = require('invariant'); var mergeFast = require('mergeFast'); +var mountSafeCallback = require('mountSafeCallback'); var precomputeStyle = require('precomputeStyle'); type MeasureOnSuccessCallback = ( @@ -52,7 +53,11 @@ var animationIDInvariant = function( var NativeMethodsMixin = { addAnimation: function(anim: number, callback?: (finished: bool) => void) { animationIDInvariant('addAnimation', anim); - RCTPOPAnimationManager.addAnimation(findNodeHandle(this), anim, callback); + RCTPOPAnimationManager.addAnimation( + findNodeHandle(this), + anim, + mountSafeCallback(this, callback) + ); }, removeAnimation: function(anim: number) { @@ -61,7 +66,10 @@ var NativeMethodsMixin = { }, measure: function(callback: MeasureOnSuccessCallback) { - RCTUIManager.measure(findNodeHandle(this), callback); + RCTUIManager.measure( + findNodeHandle(this), + mountSafeCallback(this, callback) + ); }, measureLayout: function( @@ -72,8 +80,8 @@ var NativeMethodsMixin = { RCTUIManager.measureLayout( findNodeHandle(this), relativeToNativeNode, - onFail, - onSuccess + mountSafeCallback(this, onFail), + mountSafeCallback(this, onSuccess) ); }, diff --git a/Libraries/ReactNative/ReactNativeBaseComponent.js b/Libraries/ReactNative/ReactNativeBaseComponent.js index 1db02652e..dcc31a2b3 100644 --- a/Libraries/ReactNative/ReactNativeBaseComponent.js +++ b/Libraries/ReactNative/ReactNativeBaseComponent.js @@ -12,7 +12,6 @@ 'use strict'; var NativeMethodsMixin = require('NativeMethodsMixin'); -var ReactNativeComponentMixin = require('ReactNativeComponentMixin'); var ReactNativeEventEmitter = require('ReactNativeEventEmitter'); var ReactNativeStyleAttributes = require('ReactNativeStyleAttributes'); var ReactNativeTagHandles = require('ReactNativeTagHandles'); @@ -275,8 +274,7 @@ Object.assign( ReactNativeBaseComponent.prototype, ReactMultiChild.Mixin, ReactNativeBaseComponent.Mixin, - NativeMethodsMixin, - ReactNativeComponentMixin + NativeMethodsMixin ); module.exports = ReactNativeBaseComponent; diff --git a/Libraries/ReactNative/ReactNativeBaseComponentMixin.js b/Libraries/ReactNative/ReactNativeBaseComponentMixin.js deleted file mode 100644 index 7cbf97077..000000000 --- a/Libraries/ReactNative/ReactNativeBaseComponentMixin.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Copyright (c) 2015-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactNativeComponentMixin - * @flow - */ -'use strict'; - -var findNodeHandle = require('findNodeHandle'); - -var ReactNativeComponentMixin = { - /** - * This method is deprecated; use `React.findNodeHandle` instead. - */ - getNativeNode: function() { - return findNodeHandle(this); - }, - - /** - * This method is deprecated; use `React.findNodeHandle` instead. - */ - getNodeHandle: function() { - return findNodeHandle(this); - } -}; - -module.exports = ReactNativeComponentMixin; diff --git a/Libraries/ReactNative/ReactNativeDefaultInjection.js b/Libraries/ReactNative/ReactNativeDefaultInjection.js index 93c261280..3b15a8cc9 100644 --- a/Libraries/ReactNative/ReactNativeDefaultInjection.js +++ b/Libraries/ReactNative/ReactNativeDefaultInjection.js @@ -27,7 +27,6 @@ var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy'); var ReactEmptyComponent = require('ReactEmptyComponent'); var ReactInstanceHandles = require('ReactInstanceHandles'); var ReactNativeComponentEnvironment = require('ReactNativeComponentEnvironment'); -var ReactNativeComponentMixin = require('ReactNativeComponentMixin'); var ReactNativeGlobalInteractionHandler = require('ReactNativeGlobalInteractionHandler'); var ReactNativeGlobalResponderHandler = require('ReactNativeGlobalResponderHandler'); var ReactNativeMount = require('ReactNativeMount'); @@ -90,8 +89,6 @@ function inject() { EventPluginUtils.injection.injectMount(ReactNativeMount); - ReactClass.injection.injectMixin(ReactNativeComponentMixin); - ReactNativeComponent.injection.injectTextComponentClass( ReactNativeTextComponent ); diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m index 9d2ade8e4..b4fcc6951 100644 --- a/Libraries/Text/RCTText.m +++ b/Libraries/Text/RCTText.m @@ -25,6 +25,9 @@ if ((self = [super initWithFrame:frame])) { _textStorage = [[NSTextStorage alloc] init]; + self.isAccessibilityElement = YES; + self.accessibilityTraits |= UIAccessibilityTraitStaticText; + self.opaque = NO; self.contentMode = UIViewContentModeRedraw; } @@ -127,4 +130,11 @@ return reactTag ?: self.reactTag; } +#pragma mark - Accessibility + +- (NSString *)accessibilityLabel +{ + return _textStorage.string; +} + @end diff --git a/Libraries/Text/RCTTextView.h b/Libraries/Text/RCTTextView.h index 19f2fea39..014e35315 100644 --- a/Libraries/Text/RCTTextView.h +++ b/Libraries/Text/RCTTextView.h @@ -21,8 +21,10 @@ @property (nonatomic, assign) BOOL selectTextOnFocus; @property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) BOOL automaticallyAdjustContentInsets; +@property (nonatomic, copy) NSString *text; +@property (nonatomic, strong) UIColor *textColor; @property (nonatomic, strong) UIColor *placeholderTextColor; -@property (nonatomic, assign) UIFont *font; +@property (nonatomic, strong) UIFont *font; - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; diff --git a/Libraries/Text/RCTTextView.m b/Libraries/Text/RCTTextView.m index c5947f317..fa5b2bf8a 100644 --- a/Libraries/Text/RCTTextView.m +++ b/Libraries/Text/RCTTextView.m @@ -72,13 +72,22 @@ } } +- (UIFont *)font +{ + return _textView.font; +} + - (void)setFont:(UIFont *)font { - _font = font; - _textView.font = _font; + _textView.font = font; [self updatePlaceholder]; } +- (UIColor *)textColor +{ + return _textView.textColor; +} + - (void)setTextColor:(UIColor *)textColor { _textView.textColor = textColor; @@ -106,6 +115,11 @@ [self updateFrames]; } +- (NSString *)text +{ + return _textView.text; +} + - (void)setText:(NSString *)text { if (![text isEqualToString:_textView.text]) { @@ -146,7 +160,6 @@ - (void)textViewDidBeginEditing:(UITextView *)textView { if (_clearTextOnFocus) { - [_textView setText:@""]; _textView.text = @""; [self _setPlaceholderVisibility]; } diff --git a/Libraries/Utilities/mountSafeCallback.js b/Libraries/Utilities/mountSafeCallback.js new file mode 100644 index 000000000..c065718d1 --- /dev/null +++ b/Libraries/Utilities/mountSafeCallback.js @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule mountSafeCallback + * @flow + */ +'use strict'; + +var mountSafeCallback = function(context: ReactComponent, callback: ?Function): any { + return function() { + if (!callback || !context.isMounted()) { + return; + } + return callback.apply(context, arguments); + }; +}; + +module.exports = mountSafeCallback; diff --git a/Libraries/WebSocket/RCTSRWebSocket.h b/Libraries/WebSocket/RCTSRWebSocket.h new file mode 100644 index 000000000..ccaa46455 --- /dev/null +++ b/Libraries/WebSocket/RCTSRWebSocket.h @@ -0,0 +1,132 @@ +// +// Copyright 2012 Square Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import +#import + +typedef enum { + RCTSR_CONNECTING = 0, + RCTSR_OPEN = 1, + RCTSR_CLOSING = 2, + RCTSR_CLOSED = 3, +} RCTSRReadyState; + +typedef enum RCTSRStatusCode : NSInteger { + RCTSRStatusCodeNormal = 1000, + RCTSRStatusCodeGoingAway = 1001, + RCTSRStatusCodeProtocolError = 1002, + RCTSRStatusCodeUnhandledType = 1003, + // 1004 reserved. + RCTSRStatusNoStatusReceived = 1005, + // 1004-1006 reserved. + RCTSRStatusCodeInvalidUTF8 = 1007, + RCTSRStatusCodePolicyViolated = 1008, + RCTSRStatusCodeMessageTooBig = 1009, +} RCTSRStatusCode; + +@class RCTSRWebSocket; + +extern NSString *const RCTSRWebSocketErrorDomain; +extern NSString *const RCTSRHTTPResponseErrorKey; + +#pragma mark - RCTSRWebSocketDelegate + +@protocol RCTSRWebSocketDelegate; + +#pragma mark - RCTSRWebSocket + +@interface RCTSRWebSocket : NSObject + +@property (nonatomic, weak) id delegate; + +@property (nonatomic, readonly) RCTSRReadyState readyState; +@property (nonatomic, readonly, strong) NSURL *url; + +// This returns the negotiated protocol. +// It will be nil until after the handshake completes. +@property (nonatomic, readonly, copy) NSString *protocol; + +// Protocols should be an array of strings that turn into Sec-WebSocket-Protocol. +- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithURLRequest:(NSURLRequest *)request; + +// Some helper constructors. +- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; +- (instancetype)initWithURL:(NSURL *)url; + +// Delegate queue will be dispatch_main_queue by default. +// You cannot set both OperationQueue and dispatch_queue. +- (void)setDelegateOperationQueue:(NSOperationQueue*) queue; +- (void)setDelegateDispatchQueue:(dispatch_queue_t) queue; + +// By default, it will schedule itself on +[NSRunLoop RCTSR_networkRunLoop] using defaultModes. +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; +- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; + +// RCTSRWebSockets are intended for one-time-use only. Open should be called once and only once. +- (void)open; + +- (void)close; +- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason; + +// Send a UTF8 String or Data. +- (void)send:(id)data; + +// Send Data (can be nil) in a ping message. +- (void)sendPing:(NSData *)data; + +@end + +#pragma mark - RCTSRWebSocketDelegate + +@protocol RCTSRWebSocketDelegate + +// message will either be an NSString if the server is using text +// or NSData if the server is using binary. +- (void)webSocket:(RCTSRWebSocket *)webSocket didReceiveMessage:(id)message; + +@optional + +- (void)webSocketDidOpen:(RCTSRWebSocket *)webSocket; +- (void)webSocket:(RCTSRWebSocket *)webSocket didFailWithError:(NSError *)error; +- (void)webSocket:(RCTSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reason:(NSString *)reason wasClean:(BOOL)wasClean; +- (void)webSocket:(RCTSRWebSocket *)webSocket didReceivePong:(NSData *)pongPayload; + +@end + +#pragma mark - NSURLRequest (CertificateAdditions) + +@interface NSURLRequest (CertificateAdditions) + +@property (nonatomic, readonly, copy) NSArray *RCTSR_SSLPinnedCertificates; + +@end + +#pragma mark - NSMutableURLRequest (CertificateAdditions) + +@interface NSMutableURLRequest (CertificateAdditions) + +@property (nonatomic, copy) NSArray *RCTSR_SSLPinnedCertificates; + +@end + +#pragma mark - NSRunLoop (RCTSRWebSocket) + +@interface NSRunLoop (RCTSRWebSocket) + ++ (NSRunLoop *)RCTSR_networkRunLoop; + +@end diff --git a/Libraries/WebSocket/RCTSRWebSocket.m b/Libraries/WebSocket/RCTSRWebSocket.m new file mode 100644 index 000000000..6d36d80b2 --- /dev/null +++ b/Libraries/WebSocket/RCTSRWebSocket.m @@ -0,0 +1,1627 @@ +// +// Copyright 2012 Square Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#import "RCTSRWebSocket.h" + +#import +#import + +#import + +#import + +#import "RCTAssert.h" +#import "RCTLog.h" + +typedef NS_ENUM(NSInteger, RCTSROpCode) { + RCTSROpCodeTextFrame = 0x1, + RCTSROpCodeBinaryFrame = 0x2, + // 3-7 reserved. + RCTSROpCodeConnectionClose = 0x8, + RCTSROpCodePing = 0x9, + RCTSROpCodePong = 0xA, + // B-F reserved. +}; + +typedef struct { + BOOL fin; + // BOOL rsv1; + // BOOL rsv2; + // BOOL rsv3; + uint8_t opcode; + BOOL masked; + uint64_t payload_length; +} frame_header; + +static NSString *const RCTSRWebSocketAppendToSecKeyString = @"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + +static inline int32_t validate_dispatch_data_partial_string(NSData *data); +static inline void RCTSRFastLog(NSString *format, ...); + +@interface NSData (RCTSRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; + +@end + + +@interface NSString (RCTSRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; + +@end + + +@interface NSURL (RCTSRWebSocket) + +// The origin isn't really applicable for a native application. +// So instead, just map ws -> http and wss -> https. +- (NSString *)RCTSR_origin; + +@end + + +@interface _RCTSRRunLoopThread : NSThread + +@property (nonatomic, readonly) NSRunLoop *runLoop; + +@end + + +static NSString *newSHA1String(const char *bytes, size_t length) +{ + uint8_t md[CC_SHA1_DIGEST_LENGTH]; + + assert(length >= 0); + assert(length <= UINT32_MAX); + CC_SHA1(bytes, (CC_LONG)length, md); + + NSData *data = [NSData dataWithBytes:md length:CC_SHA1_DIGEST_LENGTH]; + return [data base64EncodedStringWithOptions:0]; +} + +@implementation NSData (RCTSRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; +{ + return newSHA1String(self.bytes, self.length); +} + +@end + + +@implementation NSString (RCTSRWebSocket) + +- (NSString *)stringBySHA1ThenBase64Encoding; +{ + return newSHA1String(self.UTF8String, self.length); +} + +@end + +NSString *const RCTSRWebSocketErrorDomain = @"RCTSRWebSocketErrorDomain"; +NSString *const RCTSRHTTPResponseErrorKey = @"HTTPResponseStatusCode"; + +// Returns number of bytes consumed. Returning 0 means you didn't match. +// Sends bytes to callback handler; +typedef size_t (^stream_scanner)(NSData *collected_data); + +typedef void (^data_callback)(RCTSRWebSocket *webSocket, NSData *data); + +@interface RCTSRIOConsumer : NSObject + +@property (nonatomic, copy, readonly) stream_scanner consumer; +@property (nonatomic, copy, readonly) data_callback handler; +@property (nonatomic, assign) size_t bytesNeeded; +@property (nonatomic, assign, readonly) BOOL readToCurrentFrame; +@property (nonatomic, assign, readonly) BOOL unmaskBytes; + +@end + +// This class is not thread-safe, and is expected to always be run on the same queue. +@interface RCTSRIOConsumerPool : NSObject + +- (instancetype)initWithBufferCapacity:(NSUInteger)poolSize NS_DESIGNATED_INITIALIZER; + +- (RCTSRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +- (void)returnConsumer:(RCTSRIOConsumer *)consumer; + +@end + +@interface RCTSRWebSocket () + +- (void)_writeData:(NSData *)data; +- (void)_closeWithProtocolError:(NSString *)message; +- (void)_failWithError:(NSError *)error; + +- (void)_disconnect; + +- (void)_readFrameNew; +- (void)_readFrameContinue; + +- (void)_pumpScanner; + +- (void)_pumpWriting; + +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback; +- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength; +- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler; +- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler; + +- (void)_sendFrameWithOpcode:(RCTSROpCode)opcode data:(id)data; + +- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; +- (void)_RCTSR_commonInit; + +- (void)_initializeStreams; +- (void)_connect; + +@property (nonatomic, assign) RCTSRReadyState readyState; + +@property (nonatomic, strong) NSOperationQueue *delegateOperationQueue; +@property (nonatomic, strong) dispatch_queue_t delegateDispatchQueue; + +@end + + +@implementation RCTSRWebSocket +{ + NSInteger _webSocketVersion; + + NSOperationQueue *_delegateOperationQueue; + dispatch_queue_t _delegateDispatchQueue; + + dispatch_queue_t _workQueue; + NSMutableArray *_consumers; + + NSInputStream *_inputStream; + NSOutputStream *_outputStream; + + NSMutableData *_readBuffer; + NSUInteger _readBufferOffset; + + NSMutableData *_outputBuffer; + NSUInteger _outputBufferOffset; + + uint8_t _currentFrameOpcode; + size_t _currentFrameCount; + size_t _readOpCount; + uint32_t _currentStringScanPosition; + NSMutableData *_currentFrameData; + + NSString *_closeReason; + + NSString *_secKey; + + BOOL _pinnedCertFound; + + uint8_t _currentReadMaskKey[4]; + size_t _currentReadMaskOffset; + + BOOL _consumerStopped; + + BOOL _closeWhenFinishedWriting; + BOOL _failed; + + BOOL _secure; + NSURLRequest *_urlRequest; + + CFHTTPMessageRef _receivedHTTPHeaders; + + BOOL _sentClose; + BOOL _didFail; + int _closeCode; + + BOOL _isPumping; + + NSMutableSet *_scheduledRunloops; + + // We use this to retain ourselves. + __strong RCTSRWebSocket *_selfRetain; + + NSArray *_requestedProtocols; + RCTSRIOConsumerPool *_consumerPool; +} + +static __strong NSData *CRLFCRLF; + ++ (void)initialize; +{ + CRLFCRLF = [[NSData alloc] initWithBytes:"\r\n\r\n" length:4]; +} + +- (instancetype)initWithURLRequest:(NSURLRequest *)request protocols:(NSArray *)protocols; +{ + if ((self = [super init])) { + assert(request.URL); + _url = request.URL; + _urlRequest = request; + + _requestedProtocols = [protocols copy]; + + [self _RCTSR_commonInit]; + } + + return self; +} + +- (instancetype)initWithURLRequest:(NSURLRequest *)request; +{ + return [self initWithURLRequest:request protocols:nil]; +} + +- (instancetype)initWithURL:(NSURL *)url; +{ + return [self initWithURL:url protocols:nil]; +} + +- (instancetype)initWithURL:(NSURL *)url protocols:(NSArray *)protocols; +{ + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url]; + return [self initWithURLRequest:request protocols:protocols]; +} + +- (void)_RCTSR_commonInit; +{ + NSString *scheme = _url.scheme.lowercaseString; + assert([scheme isEqualToString:@"ws"] || [scheme isEqualToString:@"http"] || [scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]); + + if ([scheme isEqualToString:@"wss"] || [scheme isEqualToString:@"https"]) { + _secure = YES; + } + + _readyState = RCTSR_CONNECTING; + _consumerStopped = YES; + _webSocketVersion = 13; + + _workQueue = dispatch_queue_create(NULL, DISPATCH_QUEUE_SERIAL); + + // Going to set a specific on the queue so we can validate we're on the work queue + dispatch_queue_set_specific(_workQueue, (__bridge void *)self, (__bridge void *)_workQueue, NULL); + + _delegateDispatchQueue = dispatch_get_main_queue(); + + _readBuffer = [[NSMutableData alloc] init]; + _outputBuffer = [[NSMutableData alloc] init]; + + _currentFrameData = [[NSMutableData alloc] init]; + + _consumers = [[NSMutableArray alloc] init]; + + _consumerPool = [[RCTSRIOConsumerPool alloc] init]; + + _scheduledRunloops = [[NSMutableSet alloc] init]; + + [self _initializeStreams]; + + // default handlers +} + +- (void)assertOnWorkQueue; +{ + assert(dispatch_get_specific((__bridge void *)self) == (__bridge void *)_workQueue); +} + +- (void)dealloc +{ + _inputStream.delegate = nil; + _outputStream.delegate = nil; + + [_inputStream close]; + [_outputStream close]; + + _workQueue = NULL; + + if (_receivedHTTPHeaders) { + CFRelease(_receivedHTTPHeaders); + _receivedHTTPHeaders = NULL; + } + + if (_delegateDispatchQueue) { + _delegateDispatchQueue = NULL; + } +} + +#ifndef NDEBUG + +- (void)setReadyState:(RCTSRReadyState)aReadyState; +{ + [self willChangeValueForKey:@"readyState"]; + assert(aReadyState > _readyState); + _readyState = aReadyState; + [self didChangeValueForKey:@"readyState"]; +} + +#endif + +- (void)open; +{ + assert(_url); + RCTAssert(_readyState == RCTSR_CONNECTING, @"Cannot call -(void)open on RCTSRWebSocket more than once"); + + _selfRetain = self; + + [self _connect]; +} + +// Calls block on delegate queue +- (void)_performDelegateBlock:(dispatch_block_t)block; +{ + if (_delegateOperationQueue) { + [_delegateOperationQueue addOperationWithBlock:block]; + } else { + assert(_delegateDispatchQueue); + dispatch_async(_delegateDispatchQueue, block); + } +} + +- (void)setDelegateDispatchQueue:(dispatch_queue_t)queue; +{ + _delegateDispatchQueue = queue; +} + +- (BOOL)_checkHandshake:(CFHTTPMessageRef)httpMessage; +{ + NSString *acceptHeader = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(httpMessage, CFSTR("Sec-WebSocket-Accept"))); + + if (acceptHeader == nil) { + return NO; + } + + NSString *concattedString = [_secKey stringByAppendingString:RCTSRWebSocketAppendToSecKeyString]; + NSString *expectedAccept = [concattedString stringBySHA1ThenBase64Encoding]; + + return [acceptHeader isEqualToString:expectedAccept]; +} + +- (void)_HTTPHeadersDidFinish; +{ + NSInteger responseCode = CFHTTPMessageGetResponseStatusCode(_receivedHTTPHeaders); + + if (responseCode >= 400) { + RCTSRFastLog(@"Request failed with response code %d", responseCode); + [self _failWithError:[NSError errorWithDomain:RCTSRWebSocketErrorDomain code:2132 userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat:@"received bad response code from server %ld", (long)responseCode], RCTSRHTTPResponseErrorKey:@(responseCode)}]]; + return; + } + + if(![self _checkHandshake:_receivedHTTPHeaders]) { + [self _failWithError:[NSError errorWithDomain:RCTSRWebSocketErrorDomain code:2133 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Invalid Sec-WebSocket-Accept response"]}]]; + return; + } + + NSString *negotiatedProtocol = CFBridgingRelease(CFHTTPMessageCopyHeaderFieldValue(_receivedHTTPHeaders, CFSTR("Sec-WebSocket-Protocol"))); + if (negotiatedProtocol) { + // Make sure we requested the protocol + if ([_requestedProtocols indexOfObject:negotiatedProtocol] == NSNotFound) { + [self _failWithError:[NSError errorWithDomain:RCTSRWebSocketErrorDomain code:2133 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Server specified Sec-WebSocket-Protocol that wasn't requested"]}]]; + return; + } + + _protocol = negotiatedProtocol; + } + + self.readyState = RCTSR_OPEN; + + if (!_didFail) { + [self _readFrameNew]; + } + + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocketDidOpen:)]) { + [self.delegate webSocketDidOpen:self]; + }; + }]; +} + +- (void)_readHTTPHeader; +{ + if (_receivedHTTPHeaders == NULL) { + _receivedHTTPHeaders = CFHTTPMessageCreateEmpty(NULL, NO); + } + + [self _readUntilHeaderCompleteWithCallback:^(RCTSRWebSocket *socket, NSData *data) { + CFHTTPMessageAppendBytes(_receivedHTTPHeaders, (const UInt8 *)data.bytes, data.length); + + if (CFHTTPMessageIsHeaderComplete(_receivedHTTPHeaders)) { + RCTSRFastLog(@"Finished reading headers %@", CFBridgingRelease(CFHTTPMessageCopyAllHeaderFields(_receivedHTTPHeaders))); + [socket _HTTPHeadersDidFinish]; + } else { + [socket _readHTTPHeader]; + } + }]; +} + +- (void)didConnect +{ + RCTSRFastLog(@"Connected"); + CFHTTPMessageRef request = CFHTTPMessageCreateRequest(NULL, CFSTR("GET"), (__bridge CFURLRef)_url, kCFHTTPVersion1_1); + + // Set host first so it defaults + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Host"), (__bridge CFStringRef)(_url.port ? [NSString stringWithFormat:@"%@:%@", _url.host, _url.port] : _url.host)); + + NSMutableData *keyBytes = [[NSMutableData alloc] initWithLength:16]; + SecRandomCopyBytes(kSecRandomDefault, keyBytes.length, keyBytes.mutableBytes); + _secKey = [keyBytes base64EncodedStringWithOptions:0]; + assert([_secKey length] == 24); + + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Upgrade"), CFSTR("websocket")); + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Connection"), CFSTR("Upgrade")); + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Key"), (__bridge CFStringRef)_secKey); + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Version"), (__bridge CFStringRef)[NSString stringWithFormat:@"%ld", (long)_webSocketVersion]); + + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Origin"), (__bridge CFStringRef)_url.RCTSR_origin); + + if (_requestedProtocols) { + CFHTTPMessageSetHeaderFieldValue(request, CFSTR("Sec-WebSocket-Protocol"), (__bridge CFStringRef)[_requestedProtocols componentsJoinedByString:@", "]); + } + + [_urlRequest.allHTTPHeaderFields enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { + CFHTTPMessageSetHeaderFieldValue(request, (__bridge CFStringRef)key, (__bridge CFStringRef)obj); + }]; + + NSData *message = CFBridgingRelease(CFHTTPMessageCopySerializedMessage(request)); + + CFRelease(request); + + [self _writeData:message]; + [self _readHTTPHeader]; +} + +- (void)_initializeStreams; +{ + assert(_url.port.unsignedIntValue <= UINT32_MAX); + uint32_t port = _url.port.unsignedIntValue; + if (port == 0) { + if (!_secure) { + port = 80; + } else { + port = 443; + } + } + NSString *host = _url.host; + + CFReadStreamRef readStream = NULL; + CFWriteStreamRef writeStream = NULL; + + CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)host, port, &readStream, &writeStream); + + _outputStream = CFBridgingRelease(writeStream); + _inputStream = CFBridgingRelease(readStream); + + + if (_secure) { + NSMutableDictionary *SSLOptions = [[NSMutableDictionary alloc] init]; + + [_outputStream setProperty:(__bridge id)kCFStreamSocketSecurityLevelNegotiatedSSL forKey:(__bridge id)kCFStreamPropertySocketSecurityLevel]; + + // If we're using pinned certs, don't validate the certificate chain + if ([_urlRequest RCTSR_SSLPinnedCertificates].count) { + [SSLOptions setValue:@NO forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; + } + +#if DEBUG + [SSLOptions setValue:@NO forKey:(__bridge id)kCFStreamSSLValidatesCertificateChain]; + RCTLogInfo(@"SocketRocket: In debug mode. Allowing connection to any root cert"); +#endif + + [_outputStream setProperty:SSLOptions + forKey:(__bridge id)kCFStreamPropertySSLSettings]; + } + + _inputStream.delegate = self; + _outputStream.delegate = self; +} + +- (void)_connect; +{ + if (!_scheduledRunloops.count) { + [self scheduleInRunLoop:[NSRunLoop RCTSR_networkRunLoop] forMode:NSDefaultRunLoopMode]; + } + + [_outputStream open]; + [_inputStream open]; +} + +- (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; +{ + [_outputStream scheduleInRunLoop:aRunLoop forMode:mode]; + [_inputStream scheduleInRunLoop:aRunLoop forMode:mode]; + + [_scheduledRunloops addObject:@[aRunLoop, mode]]; +} + +- (void)unscheduleFromRunLoop:(NSRunLoop *)aRunLoop forMode:(NSString *)mode; +{ + [_outputStream removeFromRunLoop:aRunLoop forMode:mode]; + [_inputStream removeFromRunLoop:aRunLoop forMode:mode]; + + [_scheduledRunloops removeObject:@[aRunLoop, mode]]; +} + +- (void)close; +{ + [self closeWithCode:RCTSRStatusCodeNormal reason:nil]; +} + +- (void)closeWithCode:(NSInteger)code reason:(NSString *)reason; +{ + assert(code); + dispatch_async(_workQueue, ^{ + if (self.readyState == RCTSR_CLOSING || self.readyState == RCTSR_CLOSED) { + return; + } + + BOOL wasConnecting = self.readyState == RCTSR_CONNECTING; + + self.readyState = RCTSR_CLOSING; + + RCTSRFastLog(@"Closing with code %d reason %@", code, reason); + + if (wasConnecting) { + [self _disconnect]; + return; + } + + size_t maxMsgSize = [reason maximumLengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + NSMutableData *mutablePayload = [[NSMutableData alloc] initWithLength:sizeof(uint16_t) + maxMsgSize]; + NSData *payload = mutablePayload; + + ((uint16_t *)mutablePayload.mutableBytes)[0] = EndianU16_BtoN(code); + + if (reason) { + NSRange remainingRange = {0}; + + NSUInteger usedLength = 0; + + BOOL success = [reason getBytes:(char *)mutablePayload.mutableBytes + sizeof(uint16_t) maxLength:payload.length - sizeof(uint16_t) usedLength:&usedLength encoding:NSUTF8StringEncoding options:NSStringEncodingConversionExternalRepresentation range:NSMakeRange(0, reason.length) remainingRange:&remainingRange]; + + assert(success); + assert(remainingRange.length == 0); + + if (usedLength != maxMsgSize) { + payload = [payload subdataWithRange:NSMakeRange(0, usedLength + sizeof(uint16_t))]; + } + } + + [self _sendFrameWithOpcode:RCTSROpCodeConnectionClose data:payload]; + }); +} + +- (void)_closeWithProtocolError:(NSString *)message; +{ + // Need to shunt this on the _callbackQueue first to see if they received any messages + [self _performDelegateBlock:^{ + [self closeWithCode:RCTSRStatusCodeProtocolError reason:message]; + dispatch_async(_workQueue, ^{ + [self _disconnect]; + }); + }]; +} + +- (void)_failWithError:(NSError *)error; +{ + dispatch_async(_workQueue, ^{ + if (self.readyState != RCTSR_CLOSED) { + _failed = YES; + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didFailWithError:)]) { + [self.delegate webSocket:self didFailWithError:error]; + } + }]; + + self.readyState = RCTSR_CLOSED; + _selfRetain = nil; + + RCTSRFastLog(@"Failing with error %@", error.localizedDescription); + + [self _disconnect]; + } + }); +} + +- (void)_writeData:(NSData *)data; +{ + [self assertOnWorkQueue]; + + if (_closeWhenFinishedWriting) { + return; + } + [_outputBuffer appendData:data]; + [self _pumpWriting]; +} + +- (void)send:(id)data; +{ + RCTAssert(self.readyState != RCTSR_CONNECTING, @"Invalid State: Cannot call send: until connection is open"); + // TODO: maybe not copy this for performance + data = [data copy]; + dispatch_async(_workQueue, ^{ + if ([data isKindOfClass:[NSString class]]) { + [self _sendFrameWithOpcode:RCTSROpCodeTextFrame data:[(NSString *)data dataUsingEncoding:NSUTF8StringEncoding]]; + } else if ([data isKindOfClass:[NSData class]]) { + [self _sendFrameWithOpcode:RCTSROpCodeBinaryFrame data:data]; + } else if (data == nil) { + [self _sendFrameWithOpcode:RCTSROpCodeTextFrame data:data]; + } else { + assert(NO); + } + }); +} + +- (void)sendPing:(NSData *)data; +{ + RCTAssert(self.readyState == RCTSR_OPEN, @"Invalid State: Cannot call send: until connection is open"); + // TODO: maybe not copy this for performance + data = [data copy] ?: [NSData data]; // It's okay for a ping to be empty + dispatch_async(_workQueue, ^{ + [self _sendFrameWithOpcode:RCTSROpCodePing data:data]; + }); +} + +- (void)handlePing:(NSData *)pingData; +{ + // Need to pingpong this off _callbackQueue first to make sure messages happen in order + [self _performDelegateBlock:^{ + dispatch_async(_workQueue, ^{ + [self _sendFrameWithOpcode:RCTSROpCodePong data:pingData]; + }); + }]; +} + +- (void)handlePong:(NSData *)pongData; +{ + RCTSRFastLog(@"Received pong"); + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didReceivePong:)]) { + [self.delegate webSocket:self didReceivePong:pongData]; + } + }]; +} + +- (void)_handleMessage:(id)message +{ + RCTSRFastLog(@"Received message"); + [self _performDelegateBlock:^{ + [self.delegate webSocket:self didReceiveMessage:message]; + }]; +} + +static inline BOOL closeCodeIsValid(int closeCode) +{ + if (closeCode < 1000) { + return NO; + } + + if (closeCode >= 1000 && closeCode <= 1011) { + if (closeCode == 1004 || + closeCode == 1005 || + closeCode == 1006) { + return NO; + } + return YES; + } + + if (closeCode >= 3000 && closeCode <= 3999) { + return YES; + } + + if (closeCode >= 4000 && closeCode <= 4999) { + return YES; + } + + return NO; +} + +// Note from RFC: +// +// If there is a body, the first two +// bytes of the body MUST be a 2-byte unsigned integer (in network byte +// order) representing a status code with value /code/ defined in +// Section 7.4. Following the 2-byte integer the body MAY contain UTF-8 +// encoded data with value /reason/, the interpretation of which is not +// defined by this specification. + +- (void)handleCloseWithData:(NSData *)data; +{ + size_t dataSize = data.length; + __block uint16_t closeCode = 0; + + RCTSRFastLog(@"Received close frame"); + + if (dataSize == 1) { + // TODO: handle error + [self _closeWithProtocolError:@"Payload for close must be larger than 2 bytes"]; + return; + } else if (dataSize >= 2) { + [data getBytes:&closeCode length:sizeof(closeCode)]; + _closeCode = EndianU16_BtoN(closeCode); + if (!closeCodeIsValid(_closeCode)) { + [self _closeWithProtocolError:[NSString stringWithFormat:@"Cannot have close code of %d", _closeCode]]; + return; + } + if (dataSize > 2) { + _closeReason = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(2, dataSize - 2)] encoding:NSUTF8StringEncoding]; + if (!_closeReason) { + [self _closeWithProtocolError:@"Close reason MUST be valid UTF-8"]; + return; + } + } + } else { + _closeCode = RCTSRStatusNoStatusReceived; + } + + [self assertOnWorkQueue]; + + if (self.readyState == RCTSR_OPEN) { + [self closeWithCode:1000 reason:nil]; + } + dispatch_async(_workQueue, ^{ + [self _disconnect]; + }); +} + +- (void)_disconnect; +{ + [self assertOnWorkQueue]; + RCTSRFastLog(@"Trying to disconnect"); + _closeWhenFinishedWriting = YES; + [self _pumpWriting]; +} + +- (void)_handleFrameWithData:(NSData *)frameData opCode:(NSInteger)opcode; +{ + // Check that the current data is valid UTF8 + + BOOL isControlFrame = (opcode == RCTSROpCodePing || opcode == RCTSROpCodePong || opcode == RCTSROpCodeConnectionClose); + if (!isControlFrame) { + [self _readFrameNew]; + } else { + dispatch_async(_workQueue, ^{ + [self _readFrameContinue]; + }); + } + + switch (opcode) { + case RCTSROpCodeTextFrame: { + NSString *str = [[NSString alloc] initWithData:frameData encoding:NSUTF8StringEncoding]; + if (str == nil && frameData) { + [self closeWithCode:RCTSRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"]; + dispatch_async(_workQueue, ^{ + [self _disconnect]; + }); + + return; + } + [self _handleMessage:str]; + break; + } + case RCTSROpCodeBinaryFrame: + [self _handleMessage:[frameData copy]]; + break; + case RCTSROpCodeConnectionClose: + [self handleCloseWithData:frameData]; + break; + case RCTSROpCodePing: + [self handlePing:frameData]; + break; + case RCTSROpCodePong: + [self handlePong:frameData]; + break; + default: + [self _closeWithProtocolError:[NSString stringWithFormat:@"Unknown opcode %ld", (long)opcode]]; + // TODO: Handle invalid opcode + break; + } +} + +- (void)_handleFrameHeader:(frame_header)frame_header curData:(NSData *)curData; +{ + assert(frame_header.opcode != 0); + + if (self.readyState != RCTSR_OPEN) { + return; + } + + BOOL isControlFrame = (frame_header.opcode == RCTSROpCodePing || frame_header.opcode == RCTSROpCodePong || frame_header.opcode == RCTSROpCodeConnectionClose); + + if (isControlFrame && !frame_header.fin) { + [self _closeWithProtocolError:@"Fragmented control frames not allowed"]; + return; + } + + if (isControlFrame && frame_header.payload_length >= 126) { + [self _closeWithProtocolError:@"Control frames cannot have payloads larger than 126 bytes"]; + return; + } + + if (!isControlFrame) { + _currentFrameOpcode = frame_header.opcode; + _currentFrameCount += 1; + } + + if (frame_header.payload_length == 0) { + if (isControlFrame) { + [self _handleFrameWithData:curData opCode:frame_header.opcode]; + } else { + if (frame_header.fin) { + [self _handleFrameWithData:_currentFrameData opCode:frame_header.opcode]; + } else { + // TODO: add assert that opcode is not a control; + [self _readFrameContinue]; + } + } + } else { + assert(frame_header.payload_length <= SIZE_T_MAX); + [self _addConsumerWithDataLength:(size_t)frame_header.payload_length callback:^(RCTSRWebSocket *socket, NSData *newData) { + if (isControlFrame) { + [socket _handleFrameWithData:newData opCode:frame_header.opcode]; + } else { + if (frame_header.fin) { + [socket _handleFrameWithData:socket->_currentFrameData opCode:frame_header.opcode]; + } else { + // TODO: add assert that opcode is not a control; + [socket _readFrameContinue]; + } + + } + } readToCurrentFrame:!isControlFrame unmaskBytes:frame_header.masked]; + } +} + +/* From RFC: + + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-------+-+-------------+-------------------------------+ + |F|R|R|R| opcode|M| Payload len | Extended payload length | + |I|S|S|S| (4) |A| (7) | (16/64) | + |N|V|V|V| |S| | (if payload len==126/127) | + | |1|2|3| |K| | | + +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + | Extended payload length continued, if payload len == 127 | + + - - - - - - - - - - - - - - - +-------------------------------+ + | |Masking-key, if MASK set to 1 | + +-------------------------------+-------------------------------+ + | Masking-key (continued) | Payload Data | + +-------------------------------- - - - - - - - - - - - - - - - + + : Payload Data continued ... : + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + | Payload Data continued ... | + +---------------------------------------------------------------+ + */ + +static const uint8_t RCTSRFinMask = 0x80; +static const uint8_t RCTSROpCodeMask = 0x0F; +static const uint8_t RCTSRRsvMask = 0x70; +static const uint8_t RCTSRMaskMask = 0x80; +static const uint8_t RCTSRPayloadLenMask = 0x7F; + +- (void)_readFrameContinue; +{ + assert((_currentFrameCount == 0 && _currentFrameOpcode == 0) || (_currentFrameCount > 0 && _currentFrameOpcode > 0)); + + [self _addConsumerWithDataLength:2 callback:^(RCTSRWebSocket *socket, NSData *data) { + __block frame_header header = {0}; + + const uint8_t *headerBuffer = data.bytes; + assert(data.length >= 2); + + if (headerBuffer[0] & RCTSRRsvMask) { + [socket _closeWithProtocolError:@"Server used RSV bits"]; + return; + } + + uint8_t receivedOpcode = (RCTSROpCodeMask &headerBuffer[0]); + + BOOL isControlFrame = (receivedOpcode == RCTSROpCodePing || receivedOpcode == RCTSROpCodePong || receivedOpcode == RCTSROpCodeConnectionClose); + + if (!isControlFrame && receivedOpcode != 0 && socket->_currentFrameCount > 0) { + [socket _closeWithProtocolError:@"all data frames after the initial data frame must have opcode 0"]; + return; + } + + if (receivedOpcode == 0 && socket->_currentFrameCount == 0) { + [socket _closeWithProtocolError:@"cannot continue a message"]; + return; + } + + header.opcode = receivedOpcode == 0 ? socket->_currentFrameOpcode : receivedOpcode; + + header.fin = !!(RCTSRFinMask &headerBuffer[0]); + + + header.masked = !!(RCTSRMaskMask &headerBuffer[1]); + header.payload_length = RCTSRPayloadLenMask & headerBuffer[1]; + + headerBuffer = NULL; + + if (header.masked) { + [socket _closeWithProtocolError:@"Client must receive unmasked data"]; + } + + size_t extra_bytes_needed = header.masked ? sizeof(_currentReadMaskKey) : 0; + + if (header.payload_length == 126) { + extra_bytes_needed += sizeof(uint16_t); + } else if (header.payload_length == 127) { + extra_bytes_needed += sizeof(uint64_t); + } + + if (extra_bytes_needed == 0) { + [socket _handleFrameHeader:header curData:socket->_currentFrameData]; + } else { + [socket _addConsumerWithDataLength:extra_bytes_needed callback:^(RCTSRWebSocket *_socket, NSData *_data) { + size_t mapped_size = _data.length; + const void *mapped_buffer = _data.bytes; + size_t offset = 0; + + if (header.payload_length == 126) { + assert(mapped_size >= sizeof(uint16_t)); + uint16_t newLen = EndianU16_BtoN(*(uint16_t *)(mapped_buffer)); + header.payload_length = newLen; + offset += sizeof(uint16_t); + } else if (header.payload_length == 127) { + assert(mapped_size >= sizeof(uint64_t)); + header.payload_length = EndianU64_BtoN(*(uint64_t *)(mapped_buffer)); + offset += sizeof(uint64_t); + } else { + assert(header.payload_length < 126 && header.payload_length >= 0); + } + + if (header.masked) { + assert(mapped_size >= sizeof(_currentReadMaskOffset) + offset); + memcpy(_socket->_currentReadMaskKey, ((uint8_t *)mapped_buffer) + offset, sizeof(_socket->_currentReadMaskKey)); + } + + [_socket _handleFrameHeader:header curData:_socket->_currentFrameData]; + } readToCurrentFrame:NO unmaskBytes:NO]; + } + } readToCurrentFrame:NO unmaskBytes:NO]; +} + +- (void)_readFrameNew; +{ + dispatch_async(_workQueue, ^{ + [_currentFrameData setLength:0]; + + _currentFrameOpcode = 0; + _currentFrameCount = 0; + _readOpCount = 0; + _currentStringScanPosition = 0; + + [self _readFrameContinue]; + }); +} + +- (void)_pumpWriting; +{ + [self assertOnWorkQueue]; + + NSUInteger dataLength = _outputBuffer.length; + if (dataLength - _outputBufferOffset > 0 && _outputStream.hasSpaceAvailable) { + NSInteger bytesWritten = [_outputStream write:_outputBuffer.bytes + _outputBufferOffset maxLength:dataLength - _outputBufferOffset]; + if (bytesWritten == -1) { + [self _failWithError:[NSError errorWithDomain:RCTSRWebSocketErrorDomain code:2145 userInfo:@{NSLocalizedDescriptionKey: @"Error writing to stream"}]]; + return; + } + + _outputBufferOffset += bytesWritten; + + if (_outputBufferOffset > 4096 && _outputBufferOffset > (_outputBuffer.length >> 1)) { + _outputBuffer = [[NSMutableData alloc] initWithBytes:(char *)_outputBuffer.bytes + _outputBufferOffset length:_outputBuffer.length - _outputBufferOffset]; + _outputBufferOffset = 0; + } + } + + if (_closeWhenFinishedWriting && + _outputBuffer.length - _outputBufferOffset == 0 && + (_inputStream.streamStatus != NSStreamStatusNotOpen && + _inputStream.streamStatus != NSStreamStatusClosed) && + !_sentClose) { + _sentClose = YES; + + [_outputStream close]; + [_inputStream close]; + + for (NSArray *runLoop in [_scheduledRunloops copy]) { + [self unscheduleFromRunLoop:runLoop[0] forMode:runLoop[1]]; + } + + if (!_failed) { + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { + [self.delegate webSocket:self didCloseWithCode:_closeCode reason:_closeReason wasClean:YES]; + } + }]; + } + + _selfRetain = nil; + } +} + +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback; +{ + [self assertOnWorkQueue]; + [self _addConsumerWithScanner:consumer callback:callback dataLength:0]; +} + +- (void)_addConsumerWithDataLength:(size_t)dataLength callback:(data_callback)callback readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +{ + [self assertOnWorkQueue]; + assert(dataLength); + + [_consumers addObject:[_consumerPool consumerWithScanner:nil handler:callback bytesNeeded:dataLength readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]]; + [self _pumpScanner]; +} + +- (void)_addConsumerWithScanner:(stream_scanner)consumer callback:(data_callback)callback dataLength:(size_t)dataLength; +{ + [self assertOnWorkQueue]; + [_consumers addObject:[_consumerPool consumerWithScanner:consumer handler:callback bytesNeeded:dataLength readToCurrentFrame:NO unmaskBytes:NO]]; + [self _pumpScanner]; +} + +static const char CRLFCRLFBytes[] = {'\r', '\n', '\r', '\n'}; + +- (void)_readUntilHeaderCompleteWithCallback:(data_callback)dataHandler; +{ + [self _readUntilBytes:CRLFCRLFBytes length:sizeof(CRLFCRLFBytes) callback:dataHandler]; +} + +- (void)_readUntilBytes:(const void *)bytes length:(size_t)length callback:(data_callback)dataHandler; +{ + // TODO: optimize so this can continue from where we last searched + stream_scanner consumer = ^size_t(NSData *data) { + __block size_t found_size = 0; + __block size_t match_count = 0; + + size_t size = data.length; + const unsigned char *buffer = data.bytes; + for (size_t i = 0; i < size; i++ ) { + if (((const unsigned char *)buffer)[i] == ((const unsigned char *)bytes)[match_count]) { + match_count += 1; + if (match_count == length) { + found_size = i + 1; + break; + } + } else { + match_count = 0; + } + } + return found_size; + }; + [self _addConsumerWithScanner:consumer callback:dataHandler]; +} + +// Returns true if did work +- (BOOL)_innerPumpScanner +{ + BOOL didWork = NO; + + if (self.readyState >= RCTSR_CLOSING) { + return didWork; + } + + if (!_consumers.count) { + return didWork; + } + + size_t curSize = _readBuffer.length - _readBufferOffset; + if (!curSize) { + return didWork; + } + + RCTSRIOConsumer *consumer = _consumers[0]; + + size_t bytesNeeded = consumer.bytesNeeded; + + size_t foundSize = 0; + if (consumer.consumer) { + NSData *tempView = [NSData dataWithBytesNoCopy:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset freeWhenDone:NO]; + foundSize = consumer.consumer(tempView); + } else { + assert(consumer.bytesNeeded); + if (curSize >= bytesNeeded) { + foundSize = bytesNeeded; + } else if (consumer.readToCurrentFrame) { + foundSize = curSize; + } + } + + NSData *slice = nil; + if (consumer.readToCurrentFrame || foundSize) { + NSRange sliceRange = NSMakeRange(_readBufferOffset, foundSize); + slice = [_readBuffer subdataWithRange:sliceRange]; + + _readBufferOffset += foundSize; + + if (_readBufferOffset > 4096 && _readBufferOffset > (_readBuffer.length >> 1)) { + _readBuffer = [[NSMutableData alloc] initWithBytes:(char *)_readBuffer.bytes + _readBufferOffset length:_readBuffer.length - _readBufferOffset]; _readBufferOffset = 0; + } + + if (consumer.unmaskBytes) { + NSMutableData *mutableSlice = [slice mutableCopy]; + + NSUInteger len = mutableSlice.length; + uint8_t *bytes = mutableSlice.mutableBytes; + + for (NSUInteger i = 0; i < len; i++) { + bytes[i] = bytes[i] ^ _currentReadMaskKey[_currentReadMaskOffset % sizeof(_currentReadMaskKey)]; + _currentReadMaskOffset += 1; + } + + slice = mutableSlice; + } + + if (consumer.readToCurrentFrame) { + [_currentFrameData appendData:slice]; + + _readOpCount += 1; + + if (_currentFrameOpcode == RCTSROpCodeTextFrame) { + // Validate UTF8 stuff. + size_t currentDataSize = _currentFrameData.length; + if (_currentFrameOpcode == RCTSROpCodeTextFrame && currentDataSize > 0) { + // TODO: Optimize this. Don't really have to copy all the data each time + + size_t scanSize = currentDataSize - _currentStringScanPosition; + + NSData *scan_data = [_currentFrameData subdataWithRange:NSMakeRange(_currentStringScanPosition, scanSize)]; + int32_t valid_utf8_size = validate_dispatch_data_partial_string(scan_data); + + if (valid_utf8_size == -1) { + [self closeWithCode:RCTSRStatusCodeInvalidUTF8 reason:@"Text frames must be valid UTF-8"]; + dispatch_async(_workQueue, ^{ + [self _disconnect]; + }); + return didWork; + } else { + _currentStringScanPosition += valid_utf8_size; + } + } + } + + consumer.bytesNeeded -= foundSize; + + if (consumer.bytesNeeded == 0) { + [_consumers removeObjectAtIndex:0]; + consumer.handler(self, nil); + [_consumerPool returnConsumer:consumer]; + didWork = YES; + } + } else if (foundSize) { + [_consumers removeObjectAtIndex:0]; + consumer.handler(self, slice); + [_consumerPool returnConsumer:consumer]; + didWork = YES; + } + } + return didWork; +} + +- (void)_pumpScanner; +{ + [self assertOnWorkQueue]; + + if (!_isPumping) { + _isPumping = YES; + } else { + return; + } + + while ([self _innerPumpScanner]) { + + } + + _isPumping = NO; +} + +//#define NOMASK + +static const size_t RCTSRFrameHeaderOverhead = 32; + +- (void)_sendFrameWithOpcode:(RCTSROpCode)opcode data:(id)data; +{ + [self assertOnWorkQueue]; + + if (nil == data) { + return; + } + + RCTAssert([data isKindOfClass:[NSData class]] || [data isKindOfClass:[NSString class]], @"NSString or NSData"); + + size_t payloadLength = [data isKindOfClass:[NSString class]] ? [(NSString *)data lengthOfBytesUsingEncoding:NSUTF8StringEncoding] : [data length]; + + NSMutableData *frame = [[NSMutableData alloc] initWithLength:payloadLength + RCTSRFrameHeaderOverhead]; + if (!frame) { + [self closeWithCode:RCTSRStatusCodeMessageTooBig reason:@"Message too big"]; + return; + } + uint8_t *frame_buffer = (uint8_t *)[frame mutableBytes]; + + // set fin + frame_buffer[0] = RCTSRFinMask | opcode; + + BOOL useMask = YES; +#ifdef NOMASK + useMask = NO; +#endif + + if (useMask) { + // set the mask and header + frame_buffer[1] |= RCTSRMaskMask; + } + + size_t frame_buffer_size = 2; + + const uint8_t *unmasked_payload = NULL; + if ([data isKindOfClass:[NSData class]]) { + unmasked_payload = (uint8_t *)[data bytes]; + } else if ([data isKindOfClass:[NSString class]]) { + unmasked_payload = (const uint8_t *)[data UTF8String]; + } else { + return; + } + + if (payloadLength < 126) { + frame_buffer[1] |= payloadLength; + } else if (payloadLength <= UINT16_MAX) { + frame_buffer[1] |= 126; + *((uint16_t *)(frame_buffer + frame_buffer_size)) = EndianU16_BtoN((uint16_t)payloadLength); + frame_buffer_size += sizeof(uint16_t); + } else { + frame_buffer[1] |= 127; + *((uint64_t *)(frame_buffer + frame_buffer_size)) = EndianU64_BtoN((uint64_t)payloadLength); + frame_buffer_size += sizeof(uint64_t); + } + + if (!useMask) { + for (size_t i = 0; i < payloadLength; i++) { + frame_buffer[frame_buffer_size] = unmasked_payload[i]; + frame_buffer_size += 1; + } + } else { + uint8_t *mask_key = frame_buffer + frame_buffer_size; + SecRandomCopyBytes(kSecRandomDefault, sizeof(uint32_t), (uint8_t *)mask_key); + frame_buffer_size += sizeof(uint32_t); + + // TODO: could probably optimize this with SIMD + for (size_t i = 0; i < payloadLength; i++) { + frame_buffer[frame_buffer_size] = unmasked_payload[i] ^ mask_key[i % sizeof(uint32_t)]; + frame_buffer_size += 1; + } + } + + assert(frame_buffer_size <= [frame length]); + frame.length = frame_buffer_size; + + [self _writeData:frame]; +} + +- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode; +{ + if (_secure && !_pinnedCertFound && (eventCode == NSStreamEventHasBytesAvailable || eventCode == NSStreamEventHasSpaceAvailable)) { + + NSArray *sslCerts = [_urlRequest RCTSR_SSLPinnedCertificates]; + if (sslCerts) { + SecTrustRef secTrust = (__bridge SecTrustRef)[aStream propertyForKey:(__bridge id)kCFStreamPropertySSLPeerTrust]; + if (secTrust) { + NSInteger numCerts = SecTrustGetCertificateCount(secTrust); + for (NSInteger i = 0; i < numCerts && !_pinnedCertFound; i++) { + SecCertificateRef cert = SecTrustGetCertificateAtIndex(secTrust, i); + NSData *certData = CFBridgingRelease(SecCertificateCopyData(cert)); + + for (id ref in sslCerts) { + SecCertificateRef trustedCert = (__bridge SecCertificateRef)ref; + NSData *trustedCertData = CFBridgingRelease(SecCertificateCopyData(trustedCert)); + + if ([trustedCertData isEqualToData:certData]) { + _pinnedCertFound = YES; + break; + } + } + } + } + + if (!_pinnedCertFound) { + dispatch_async(_workQueue, ^{ + [self _failWithError:[NSError errorWithDomain:RCTSRWebSocketErrorDomain code:23556 userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"Invalid server cert"]}]]; + }); + return; + } + } + } + + dispatch_async(_workQueue, ^{ + switch (eventCode) { + case NSStreamEventOpenCompleted: { + RCTSRFastLog(@"NSStreamEventOpenCompleted %@", aStream); + if (self.readyState >= RCTSR_CLOSING) { + return; + } + assert(_readBuffer); + + if (self.readyState == RCTSR_CONNECTING && aStream == _inputStream) { + [self didConnect]; + } + [self _pumpWriting]; + [self _pumpScanner]; + break; + } + + case NSStreamEventErrorOccurred: { + RCTSRFastLog(@"NSStreamEventErrorOccurred %@ %@", aStream, [[aStream streamError] copy]); + // TODO: specify error better! + [self _failWithError:aStream.streamError]; + _readBufferOffset = 0; + [_readBuffer setLength:0]; + break; + + } + + case NSStreamEventEndEncountered: { + [self _pumpScanner]; + RCTSRFastLog(@"NSStreamEventEndEncountered %@", aStream); + if (aStream.streamError) { + [self _failWithError:aStream.streamError]; + } else { + if (self.readyState != RCTSR_CLOSED) { + self.readyState = RCTSR_CLOSED; + _selfRetain = nil; + } + + if (!_sentClose && !_failed) { + _sentClose = YES; + // If we get closed in this state it's probably not clean because we should be sending this when we send messages + [self _performDelegateBlock:^{ + if ([self.delegate respondsToSelector:@selector(webSocket:didCloseWithCode:reason:wasClean:)]) { + [self.delegate webSocket:self didCloseWithCode:RCTSRStatusCodeGoingAway reason:@"Stream end encountered" wasClean:NO]; + } + }]; + } + } + + break; + } + + case NSStreamEventHasBytesAvailable: { + RCTSRFastLog(@"NSStreamEventHasBytesAvailable %@", aStream); + const int bufferSize = 2048; + uint8_t buffer[bufferSize]; + + while (_inputStream.hasBytesAvailable) { + NSInteger bytes_read = [_inputStream read:buffer maxLength:bufferSize]; + + if (bytes_read > 0) { + [_readBuffer appendBytes:buffer length:bytes_read]; + } else if (bytes_read < 0) { + [self _failWithError:_inputStream.streamError]; + } + + if (bytes_read != bufferSize) { + break; + } + }; + [self _pumpScanner]; + break; + } + + case NSStreamEventHasSpaceAvailable: { + RCTSRFastLog(@"NSStreamEventHasSpaceAvailable %@", aStream); + [self _pumpWriting]; + break; + } + + default: + RCTSRFastLog(@"(default) %@", aStream); + break; + } + }); +} + +@end + +@implementation RCTSRIOConsumer + +- (void)setupWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +{ + _consumer = [scanner copy]; + _handler = [handler copy]; + _bytesNeeded = bytesNeeded; + _readToCurrentFrame = readToCurrentFrame; + _unmaskBytes = unmaskBytes; + assert(_consumer || _bytesNeeded); +} + +@end + +@implementation RCTSRIOConsumerPool +{ + NSUInteger _poolSize; + NSMutableArray *_bufferedConsumers; +} + +- (instancetype)initWithBufferCapacity:(NSUInteger)poolSize; +{ + if ((self = [super init])) { + _poolSize = poolSize; + _bufferedConsumers = [[NSMutableArray alloc] initWithCapacity:poolSize]; + } + return self; +} + +- (instancetype)init +{ + return [self initWithBufferCapacity:8]; +} + +- (RCTSRIOConsumer *)consumerWithScanner:(stream_scanner)scanner handler:(data_callback)handler bytesNeeded:(size_t)bytesNeeded readToCurrentFrame:(BOOL)readToCurrentFrame unmaskBytes:(BOOL)unmaskBytes; +{ + RCTSRIOConsumer *consumer = nil; + if (_bufferedConsumers.count) { + consumer = [_bufferedConsumers lastObject]; + [_bufferedConsumers removeLastObject]; + } else { + consumer = [[RCTSRIOConsumer alloc] init]; + } + + [consumer setupWithScanner:scanner handler:handler bytesNeeded:bytesNeeded readToCurrentFrame:readToCurrentFrame unmaskBytes:unmaskBytes]; + + return consumer; +} + +- (void)returnConsumer:(RCTSRIOConsumer *)consumer; +{ + if (_bufferedConsumers.count < _poolSize) { + [_bufferedConsumers addObject:consumer]; + } +} + +@end + +@implementation NSURLRequest (CertificateAdditions) + +- (NSArray *)RCTSR_SSLPinnedCertificates; +{ + return [NSURLProtocol propertyForKey:@"RCTSR_SSLPinnedCertificates" inRequest:self]; +} + +@end + +@implementation NSMutableURLRequest (CertificateAdditions) + +- (NSArray *)RCTSR_SSLPinnedCertificates; +{ + return [NSURLProtocol propertyForKey:@"RCTSR_SSLPinnedCertificates" inRequest:self]; +} + +- (void)setRCTSR_SSLPinnedCertificates:(NSArray *)RCTSR_SSLPinnedCertificates; +{ + [NSURLProtocol setProperty:RCTSR_SSLPinnedCertificates forKey:@"RCTSR_SSLPinnedCertificates" inRequest:self]; +} + +@end + +@implementation NSURL (RCTSRWebSocket) + +- (NSString *)RCTSR_origin; +{ + NSString *scheme = [self.scheme lowercaseString]; + + if ([scheme isEqualToString:@"wss"]) { + scheme = @"https"; + } else if ([scheme isEqualToString:@"ws"]) { + scheme = @"http"; + } + + if (self.port) { + return [NSString stringWithFormat:@"%@://%@:%@/", scheme, self.host, self.port]; + } else { + return [NSString stringWithFormat:@"%@://%@/", scheme, self.host]; + } +} + +@end + +//#define RCTSR_ENABLE_LOG + +static inline void RCTSRFastLog(NSString *format, ...) +{ +#ifdef RCTSR_ENABLE_LOG + __block va_list arg_list; + va_start (arg_list, format); + + NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list]; + + va_end(arg_list); + + RCTLogInfo(@"[RCTSR] %@", formattedString); +#endif +} + +// This is a hack, and probably not optimal +static inline int32_t validate_dispatch_data_partial_string(NSData *data) +{ + static const int maxCodepointSize = 3; + + for (int i = 0; i < maxCodepointSize; i++) { + NSString *str = [[NSString alloc] initWithBytesNoCopy:(char *)data.bytes length:data.length - i encoding:NSUTF8StringEncoding freeWhenDone:NO]; + if (str) { + return (int32_t)data.length - i; + } + } + + return -1; +} + +static _RCTSRRunLoopThread *networkThread = nil; +static NSRunLoop *networkRunLoop = nil; + +@implementation NSRunLoop (RCTSRWebSocket) + ++ (NSRunLoop *)RCTSR_networkRunLoop +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + networkThread = [[_RCTSRRunLoopThread alloc] init]; + networkThread.name = @"com.squareup.SocketRocket.NetworkThread"; + [networkThread start]; + networkRunLoop = networkThread.runLoop; + }); + + return networkRunLoop; +} + +@end + +@implementation _RCTSRRunLoopThread +{ + dispatch_group_t _waitGroup; +} + +@synthesize runLoop = _runLoop; + +- (instancetype)init +{ + if ((self = [super init])) { + _waitGroup = dispatch_group_create(); + dispatch_group_enter(_waitGroup); + } + return self; +} + +- (void)main; +{ + @autoreleasepool { + _runLoop = [NSRunLoop currentRunLoop]; + dispatch_group_leave(_waitGroup); + + NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate distantFuture] interval:0.0 target:nil selector:nil userInfo:nil repeats:NO]; + [_runLoop addTimer:timer forMode:NSDefaultRunLoopMode]; + + while ([_runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) { } + assert(NO); + } +} + +- (NSRunLoop *)runLoop; +{ + dispatch_group_wait(_waitGroup, DISPATCH_TIME_FOREVER); + return _runLoop; +} + +@end diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.h b/Libraries/WebSocket/RCTWebSocketExecutor.h similarity index 88% rename from Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.h rename to Libraries/WebSocket/RCTWebSocketExecutor.h index 9993cbc5a..2c17541f1 100644 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.h +++ b/Libraries/WebSocket/RCTWebSocketExecutor.h @@ -15,7 +15,7 @@ @interface RCTWebSocketExecutor : NSObject -- (instancetype)initWithURL:(NSURL *)URL; +- (instancetype)initWithURL:(NSURL *)URL NS_DESIGNATED_INITIALIZER; @end diff --git a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m b/Libraries/WebSocket/RCTWebSocketExecutor.m similarity index 89% rename from Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m rename to Libraries/WebSocket/RCTWebSocketExecutor.m index 16a378ccf..97095a5c9 100644 --- a/Libraries/RCTWebSocketDebugger/RCTWebSocketExecutor.m +++ b/Libraries/WebSocket/RCTWebSocketExecutor.m @@ -16,16 +16,17 @@ #import "RCTLog.h" #import "RCTSparseArray.h" #import "RCTUtils.h" -#import "SRWebSocket.h" +#import "RCTSRWebSocket.h" -typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply); +typedef void (^RCTWSMessageCallback)(NSError *error, NSDictionary *reply); + +@interface RCTWebSocketExecutor () -@interface RCTWebSocketExecutor () @end @implementation RCTWebSocketExecutor { - SRWebSocket *_socket; + RCTSRWebSocket *_socket; dispatch_queue_t _jsQueue; RCTSparseArray *_callbacks; dispatch_semaphore_t _socketOpenSemaphore; @@ -42,7 +43,7 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply); if (self = [super init]) { _jsQueue = dispatch_queue_create("com.facebook.React.WebSocketExecutor", DISPATCH_QUEUE_SERIAL); - _socket = [[SRWebSocket alloc] initWithURL:URL]; + _socket = [[RCTSRWebSocket alloc] initWithURL:URL]; _socket.delegate = self; _callbacks = [[RCTSparseArray alloc] init]; _injectedObjects = [[NSMutableDictionary alloc] init]; @@ -95,28 +96,28 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply); return runtimeIsReady == 0 && initError == nil; } -- (void)webSocket:(SRWebSocket *)webSocket didReceiveMessage:(id)message +- (void)webSocket:(RCTSRWebSocket *)webSocket didReceiveMessage:(id)message { NSError *error = nil; NSDictionary *reply = RCTJSONParse(message, &error); NSNumber *messageID = reply[@"replyID"]; - WSMessageCallback callback = _callbacks[messageID]; + RCTWSMessageCallback callback = _callbacks[messageID]; if (callback) { callback(error, reply); } } -- (void)webSocketDidOpen:(SRWebSocket *)webSocket +- (void)webSocketDidOpen:(RCTSRWebSocket *)webSocket { dispatch_semaphore_signal(_socketOpenSemaphore); } -- (void)webSocket:(SRWebSocket *)webSocket didFailWithError:(NSError *)error +- (void)webSocket:(RCTSRWebSocket *)webSocket didFailWithError:(NSError *)error { RCTLogError(@"WebSocket connection failed with error %@", error); } -- (void)sendMessage:(NSDictionary *)message context:(NSNumber *)executorID waitForReply:(WSMessageCallback)callback +- (void)sendMessage:(NSDictionary *)message context:(NSNumber *)executorID waitForReply:(RCTWSMessageCallback)callback { static NSUInteger lastID = 10000; @@ -190,7 +191,7 @@ typedef void (^WSMessageCallback)(NSError *error, NSDictionary *reply); - (BOOL)isValid { - return _socket != nil && _socket.readyState == SR_OPEN; + return _socket != nil && _socket.readyState == RCTSR_OPEN; } - (void)dealloc diff --git a/Libraries/WebSocket/RCTWebSocketManager.h b/Libraries/WebSocket/RCTWebSocketManager.h new file mode 100644 index 000000000..196486f25 --- /dev/null +++ b/Libraries/WebSocket/RCTWebSocketManager.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTBridgeModule.h" + +@interface RCTWebSocketManager : NSObject + +@end diff --git a/Libraries/WebSocket/RCTWebSocketManager.m b/Libraries/WebSocket/RCTWebSocketManager.m new file mode 100644 index 000000000..a403e0a0a --- /dev/null +++ b/Libraries/WebSocket/RCTWebSocketManager.m @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +#import "RCTWebSocketManager.h" + +#import "RCTBridge.h" +#import "RCTEventDispatcher.h" +#import "RCTSRWebSocket.h" +#import "RCTSparseArray.h" + +@implementation RCTSRWebSocket (React) + +- (NSNumber *)reactTag +{ + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setReactTag:(NSNumber *)reactTag +{ + objc_setAssociatedObject(self, @selector(reactTag), reactTag, OBJC_ASSOCIATION_COPY_NONATOMIC); +} + +@end + +@interface RCTWebSocketManager () + +@end + +@implementation RCTWebSocketManager +{ + RCTSparseArray *_sockets; +} + +RCT_EXPORT_MODULE() + +@synthesize bridge = _bridge; + +- (instancetype)init +{ + if ((self = [super init])) { + _sockets = [[RCTSparseArray alloc] init]; + } + return self; +} + +- (void)dealloc +{ + for (RCTSRWebSocket *socket in _sockets.allObjects) { + socket.delegate = nil; + [socket close]; + } +} + +RCT_EXPORT_METHOD(connect:(NSURL *)URL socketID:(NSNumber *)socketID) +{ + RCTSRWebSocket *webSocket = [[RCTSRWebSocket alloc] initWithURL:URL]; + webSocket.delegate = self; + webSocket.reactTag = socketID; + _sockets[socketID] = webSocket; + [webSocket open]; +} + +RCT_EXPORT_METHOD(send:(NSString *)message socketID:(NSNumber *)socketID) +{ + [_sockets[socketID] send:message]; +} + +RCT_EXPORT_METHOD(close:(NSNumber *)socketID) +{ + [_sockets[socketID] close]; + _sockets[socketID] = nil; +} + +#pragma mark - RCTSRWebSocketDelegate methods + +- (void)webSocket:(RCTSRWebSocket *)webSocket didReceiveMessage:(id)message +{ + [_bridge.eventDispatcher sendDeviceEventWithName:@"websocketMessage" body:@{ + @"data": message, + @"id": webSocket.reactTag + }]; +} + +- (void)webSocketDidOpen:(RCTSRWebSocket *)webSocket +{ + [_bridge.eventDispatcher sendDeviceEventWithName:@"websocketOpen" body:@{ + @"id": webSocket.reactTag + }]; +} + +- (void)webSocket:(RCTSRWebSocket *)webSocket didFailWithError:(NSError *)error +{ + [_bridge.eventDispatcher sendDeviceEventWithName:@"websocketFailed" body:@{ + @"message":[error localizedDescription], + @"id": webSocket.reactTag + }]; +} + +- (void)webSocket:(RCTSRWebSocket *)webSocket didCloseWithCode:(NSInteger)code + reason:(NSString *)reason wasClean:(BOOL)wasClean +{ + [_bridge.eventDispatcher sendDeviceEventWithName:@"websocketClosed" body:@{ + @"code": @(code), + @"reason": reason, + @"clean": @(wasClean), + @"id": webSocket.reactTag + }]; +} + +@end diff --git a/Libraries/WebSocket/WebSocket.android.js b/Libraries/WebSocket/WebSocket.android.js new file mode 100644 index 000000000..301f92f49 --- /dev/null +++ b/Libraries/WebSocket/WebSocket.android.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule WebSocket + * + */ +'use strict'; + +var WebSocketBase = require('WebSocketBase'); + +class WebSocket extends WebSocketBase { + + connectToSocketImpl(url: string): void { + console.warn('WebSocket is not yet supported on Android'); + } + + closeConnectionImpl(): void{ + console.warn('WebSocket is not yet supported on Android'); + } + + cancelConnectionImpl(): void { + console.warn('WebSocket is not yet supported on Android'); + } + + sendStringImpl(message: string): void { + console.warn('WebSocket is not yet supported on Android'); + } + + sendArrayBufferImpl(): void { + console.warn('WebSocket is not yet supported on Android'); + } +} + +module.exports = WebSocket; diff --git a/Libraries/WebSocket/WebSocket.ios.js b/Libraries/WebSocket/WebSocket.ios.js new file mode 100644 index 000000000..e48d7882b --- /dev/null +++ b/Libraries/WebSocket/WebSocket.ios.js @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule WebSocket + * + */ +'use strict'; + +var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var RCTWebSocketManager = require('NativeModules').WebSocketManager; + +var WebSocketBase = require('WebSocketBase'); + +var WebSocketId = 0; + +class WebSocket extends WebSocketBase { + _socketId: number; + _subs: any; + + connectToSocketImpl(url: string): void { + this._socketId = WebSocketId++; + RCTWebSocketManager.connect(url, this._socketId); + this._registerEvents(this._socketId); + } + + closeConnectionImpl(): void { + RCTWebSocketManager.close(this._socketId); + } + + cancelConnectionImpl(): void { + RCTWebSocketManager.close(this._socketId); + } + + sendStringImpl(message: string): void { + RCTWebSocketManager.send(message, this._socketId); + } + + sendArrayBufferImpl(): void { + // TODO + console.warn('Sending ArrayBuffers is not yet supported'); + } + + _unregisterEvents(): void { + this._subs.forEach(e => e.remove()); + this._subs = []; + } + + _registerEvents(id: number): void { + this._subs = [ + RCTDeviceEventEmitter.addListener( + 'websocketMessage', + function(ev) { + if (ev.id !== id) { + return; + } + this.onmessage && this.onmessage({ + data: ev.data + }); + }.bind(this) + ), + RCTDeviceEventEmitter.addListener( + 'websocketOpen', + function(ev) { + if (ev.id !== id) { + return; + } + this.readyState = this.OPEN; + this.onopen && this.onopen(); + }.bind(this) + ), + RCTDeviceEventEmitter.addListener( + 'websocketClosed', + function(ev) { + if (ev.id !== id) { + return; + } + this.readyState = this.CLOSED; + this.onclose && this.onclose(ev); + this._unregisterEvents(); + RCTWebSocketManager.close(id); + }.bind(this) + ), + RCTDeviceEventEmitter.addListener( + 'websocketFailed', + function(ev) { + if (ev.id !== id) { + return; + } + this.onerror && this.onerror(new Error(ev.message)); + this._unregisterEvents(); + RCTWebSocketManager.close(id); + }.bind(this) + ) + ]; + } + +} + +module.exports = WebSocket; diff --git a/Libraries/WebSocket/WebSocketBase.js b/Libraries/WebSocket/WebSocketBase.js new file mode 100644 index 000000000..73cc62cc9 --- /dev/null +++ b/Libraries/WebSocket/WebSocketBase.js @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule WebSocketBase + * + */ +'use strict'; + +/** + * Shared base for platform-specific WebSocket implementations. + */ +class WebSocketBase { + CONNECTING: number; + OPEN: number; + CLOSING: number; + CLOSED: number; + + onclose: ?Function; + onerror: ?Function; + onmessage: ?Function; + onopen: ?Function; + + binaryType: ?string; + bufferedAmount: number; + extension: ?string; + protocol: ?string; + readyState: number; + url: ?string; + + constructor(url: string, protocols: ?any) { + this.CONNECTING = 0; + this.OPEN = 1; + this.CLOSING = 2; + this.CLOSED = 3; + + if (!protocols) { + protocols = []; + } + + this.connectToSocketImpl(url); + } + + close(): void { + if (this.readyState === WebSocketBase.CLOSING || + this.readyState === WebSocketBase.CLOSED) { + return; + } + + if (this.readyState === WebSocketBase.CONNECTING) { + this.cancelConnectionImpl(); + } + + this.closeConnectionImpl(); + } + + send(data: any): void { + if (this.readyState === WebSocketBase.CONNECTING) { + throw new Error('INVALID_STATE_ERR'); + } + + if (typeof data === 'string') { + this.sendStringImpl(data); + } else if (data instanceof ArrayBuffer) { + this.sendArrayBufferImpl(data); + } else { + throw new Error('Not supported data type'); + } + } + + closeConnectionImpl(): void { + throw new Error('Subclass must define closeConnectionImpl method'); + } + + connectToSocketImpl(): void { + throw new Error('Subclass must define connectToSocketImpl method'); + } + + cancelConnectionImpl(): void { + throw new Error('Subclass must define cancelConnectionImpl method'); + } + + sendStringImpl(): void { + throw new Error('Subclass must define sendStringImpl method'); + } + + sendArrayBufferImpl(): void { + throw new Error('Subclass must define sendArrayBufferImpl method'); + } + +} + +module.exports = WebSocketBase; diff --git a/React.podspec b/React.podspec index 159072b39..32f21edad 100644 --- a/React.podspec +++ b/React.podspec @@ -80,10 +80,10 @@ Pod::Spec.new do |s| ss.preserve_paths = "Libraries/PushNotificationIOS/*.js" end - s.subspec 'RCTWebSocketDebugger' do |ss| + s.subspec 'RCTSettings' do |ss| ss.dependency 'React/Core' - ss.libraries = 'icucore' - ss.source_files = "Libraries/RCTWebSocketDebugger/*.{h,m}" + ss.source_files = "Libraries/Settings/*.{h,m}" + ss.preserve_paths = "Libraries/Settings/*.js" end s.subspec 'RCTText' do |ss| @@ -97,4 +97,10 @@ Pod::Spec.new do |s| ss.source_files = "Libraries/Vibration/*.{h,m}" ss.preserve_paths = "Libraries/Vibration/*.js" end + + s.subspec 'RCTWebSocket' do |ss| + ss.dependency 'React/Core' + ss.source_files = "Libraries/WebSocket/*.{h,m}" + ss.preserve_paths = "Libraries/WebSocket/*.js" + end end diff --git a/React/Base/RCTDevMenu.m b/React/Base/RCTDevMenu.m index 80964e938..4e05f0e54 100644 --- a/React/Base/RCTDevMenu.m +++ b/React/Base/RCTDevMenu.m @@ -81,7 +81,7 @@ RCT_EXPORT_MODULE() object:nil]; [notificationCenter addObserver:self - selector:@selector(updateSettings) + selector:@selector(settingsDidChange) name:NSUserDefaultsDidChangeNotification object:nil]; @@ -94,13 +94,11 @@ RCT_EXPORT_MODULE() _settings = [[NSMutableDictionary alloc] init]; // Delay setup until after Bridge init - __weak RCTDevMenu *weakSelf = self; - dispatch_async(dispatch_get_main_queue(), ^{ - [weakSelf updateSettings]; - }); + [self settingsDidChange]; #if TARGET_IPHONE_SIMULATOR + __weak RCTDevMenu *weakSelf = self; RCTKeyCommands *commands = [RCTKeyCommands sharedInstance]; // Toggle debug menu @@ -127,6 +125,15 @@ RCT_EXPORT_MODULE() return dispatch_get_main_queue(); } +- (void)settingsDidChange +{ + // Needed to prevent a race condition when reloading + __weak RCTDevMenu *weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + [weakSelf updateSettings]; + }); +} + - (void)updateSettings { NSDictionary *settings = [_defaults objectForKey:RCTDevMenuSettingsKey]; @@ -269,6 +276,14 @@ RCT_EXPORT_METHOD(reload) } case 1: { Class cls = NSClassFromString(@"RCTWebSocketExecutor"); + if (!cls) { + [[[UIAlertView alloc] initWithTitle:@"Chrome Debugger Unavailable" + message:@"You need to include the RCTWebSocket library to enable Chrome debugging" + delegate:nil + cancelButtonTitle:@"OK" + otherButtonTitles:nil] show]; + return; + } self.executorClass = (_executorClass == cls) ? Nil : cls; break; } diff --git a/React/Views/RCTNavItem.m b/React/Views/RCTNavItem.m index 56346a363..f875e6aa5 100644 --- a/React/Views/RCTNavItem.m +++ b/React/Views/RCTNavItem.m @@ -56,7 +56,7 @@ - (void)setLeftButtonIcon:(UIImage *)leftButtonIcon { _leftButtonIcon = leftButtonIcon; - _leftButtonIcon = nil; + _leftButtonItem = nil; } - (UIBarButtonItem *)leftButtonItem diff --git a/package.json b/package.json index 0421c96a8..e47335eef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native", - "version": "0.4.2", + "version": "0.4.1", "description": "A framework for building native apps using React", "repository": { "type": "git", diff --git a/packager/package.json b/packager/package.json index 9ffaf9e70..3eaf2f3b9 100644 --- a/packager/package.json +++ b/packager/package.json @@ -25,7 +25,7 @@ }, "dependencies": {}, "devDependencies": { - "jest-cli": "0.2.1", + "jest-cli": "0.4.3", "eslint": "0.9.2" } } diff --git a/packager/packager.js b/packager/packager.js index 48552d93e..eb90b04e0 100644 --- a/packager/packager.js +++ b/packager/packager.js @@ -200,7 +200,12 @@ function getAppMiddleware(options) { cacheVersion: '2', transformModulePath: require.resolve('./transformer.js'), assetRoots: options.assetRoots, - assetExts: ['png', 'jpeg', 'jpg'] + assetExts: ['png', 'jpeg', 'jpg'], + polyfillModuleNames: [ + require.resolve( + '../Libraries/JavaScriptAppEngine/polyfills/document.js' + ), + ], }); } diff --git a/packager/react-packager/src/JSTransformer/Cache.js b/packager/react-packager/src/JSTransformer/Cache.js index 4761d15ed..584077b6c 100644 --- a/packager/react-packager/src/JSTransformer/Cache.js +++ b/packager/react-packager/src/JSTransformer/Cache.js @@ -31,6 +31,10 @@ var validateOpts = declareOpts({ type: 'array', required: true, }, + transformModulePath: { + type:'string', + required: true, + }, }); module.exports = Cache; @@ -162,6 +166,8 @@ function cacheFilePath(options) { var cacheVersion = options.cacheVersion || '0'; hash.update(cacheVersion); + hash.update(options.transformModulePath); + var name = 'react-packager-cache-' + hash.digest('hex'); return path.join(tmpdir, name); } diff --git a/packager/react-packager/src/JSTransformer/__tests__/Cache-test.js b/packager/react-packager/src/JSTransformer/__tests__/Cache-test.js index 7ad658183..f91490ba0 100644 --- a/packager/react-packager/src/JSTransformer/__tests__/Cache-test.js +++ b/packager/react-packager/src/JSTransformer/__tests__/Cache-test.js @@ -32,10 +32,14 @@ describe('JSTransformer Cache', function() { describe('getting/setting', function() { it('calls loader callback for uncached file', function() { - var cache = new Cache({projectRoots: ['/rootDir']}); + var cache = new Cache({ + projectRoots: ['/rootDir'], + transformModulePath: 'x.js', + }); var loaderCb = jest.genMockFn().mockImpl(function() { return Promise.resolve(); }); + cache.get('/rootDir/someFile', loaderCb); expect(loaderCb).toBeCalledWith('/rootDir/someFile'); }); @@ -48,10 +52,15 @@ describe('JSTransformer Cache', function() { } }); }); - var cache = new Cache({projectRoots: ['/rootDir']}); + + var cache = new Cache({ + projectRoots: ['/rootDir'], + transformModulePath: 'x.js', + }); var loaderCb = jest.genMockFn().mockImpl(function() { return Promise.resolve('lol'); }); + return cache.get('/rootDir/someFile', loaderCb).then(function(value) { expect(value).toBe('lol'); }); @@ -65,10 +74,15 @@ describe('JSTransformer Cache', function() { } }); }); - var cache = new Cache({projectRoots: ['/rootDir']}); + + var cache = new Cache({ + projectRoots: ['/rootDir'], + transformModulePath: 'x.js', + }); var loaderCb = jest.genMockFn().mockImpl(function() { return Promise.resolve('lol'); }); + return cache.get('/rootDir/someFile', loaderCb).then(function() { var shouldNotBeCalled = jest.genMockFn(); return cache.get('/rootDir/someFile', shouldNotBeCalled) @@ -126,8 +140,12 @@ describe('JSTransformer Cache', function() { }); pit('should load cache from disk', function() { - var cache = new Cache({projectRoots: ['/rootDir']}); + var cache = new Cache({ + projectRoots: ['/rootDir'], + transformModulePath: 'x.js', + }); var loaderCb = jest.genMockFn(); + return cache.get('/rootDir/someFile', loaderCb).then(function(value) { expect(loaderCb).not.toBeCalled(); expect(value).toBe('oh hai'); @@ -152,7 +170,10 @@ describe('JSTransformer Cache', function() { return 123; }; - var cache = new Cache({projectRoots: ['/rootDir']}); + var cache = new Cache({ + projectRoots: ['/rootDir'], + transformModulePath: 'x.js', + }); var loaderCb = jest.genMockFn().mockImpl(function() { return Promise.resolve('new value'); }); @@ -193,7 +214,11 @@ describe('JSTransformer Cache', function() { }); }); - var cache = new Cache({projectRoots: ['/rootDir']}); + var cache = new Cache({ + projectRoots: ['/rootDir'], + transformModulePath: 'x.js', + }); + cache.get('/rootDir/bar', function() { return Promise.resolve('bar value'); }); diff --git a/packager/react-packager/src/JSTransformer/index.js b/packager/react-packager/src/JSTransformer/index.js index 33e017037..2dc5e20bd 100644 --- a/packager/react-packager/src/JSTransformer/index.js +++ b/packager/react-packager/src/JSTransformer/index.js @@ -60,6 +60,7 @@ function Transformer(options) { resetCache: options.resetCache, cacheVersion: options.cacheVersion, projectRoots: options.projectRoots, + transformModulePath: options.transformModulePath, }); if (options.transformModulePath != null) {