From 9cd6369d97cd0fb7c7a915336c773b1b3276ee37 Mon Sep 17 00:00:00 2001 From: Mike Schore Date: Wed, 18 Dec 2013 17:35:04 -0800 Subject: [PATCH] Open source release --- .gitignore | 13 + .travis.yml | 4 + CONTRIBUTING.md | 68 + LICENSE | 202 ++ README.md | 82 + SPDY.iphoneos/SPDY.iphoneos-Prefix.pch | 7 + .../SPDY.iphonesimulator-Prefix.pch | 7 + SPDY.macosx/SPDY.macosx-Prefix.pch | 7 + SPDY.xcodeproj/project.pbxproj | 1159 +++++++++++ .../contents.xcworkspacedata | 7 + .../xcschemes/SPDY.iphoneos.arm64.xcscheme | 59 + .../xcschemes/SPDY.iphoneos.xcscheme | 59 + .../xcschemes/SPDY.iphonesimulator.xcscheme | 59 + .../xcschemes/SPDY.macosx.xcscheme | 59 + .../xcshareddata/xcschemes/SPDY.xcscheme | 59 + .../xcschemes/SPDYUnitTests.xcscheme | 69 + SPDY/NSURLRequest+SPDYURLRequest.h | 54 + SPDY/NSURLRequest+SPDYURLRequest.m | 159 ++ SPDY/SPDY-Info.plist | 30 + SPDY/SPDY-Prefix.pch | 8 + SPDY/SPDYCommonLogger.h | 49 + SPDY/SPDYCommonLogger.m | 62 + SPDY/SPDYDefinitions.h | 79 + SPDY/SPDYError.h | 61 + SPDY/SPDYFrame.h | 69 + SPDY/SPDYFrame.m | 69 + SPDY/SPDYFrameDecoder.h | 39 + SPDY/SPDYFrameDecoder.m | 523 +++++ SPDY/SPDYFrameEncoder.h | 33 + SPDY/SPDYFrameEncoder.m | 329 +++ SPDY/SPDYHeaderBlockCompressor.h | 17 + SPDY/SPDYHeaderBlockCompressor.m | 86 + SPDY/SPDYHeaderBlockDecompressor.h | 17 + SPDY/SPDYHeaderBlockDecompressor.m | 97 + SPDY/SPDYLogger.h | 21 + SPDY/SPDYOrigin.h | 30 + SPDY/SPDYOrigin.m | 124 ++ SPDY/SPDYProtocol.h | 139 ++ SPDY/SPDYProtocol.m | 245 +++ SPDY/SPDYSession.h | 25 + SPDY/SPDYSession.m | 866 ++++++++ SPDY/SPDYSessionManager.h | 21 + SPDY/SPDYSessionManager.m | 76 + SPDY/SPDYSettingsStore.h | 21 + SPDY/SPDYSettingsStore.m | 115 ++ SPDY/SPDYSocket.h | 354 ++++ SPDY/SPDYSocket.m | 1819 +++++++++++++++++ SPDY/SPDYStream.h | 51 + SPDY/SPDYStream.m | 488 +++++ SPDY/SPDYStreamManager.h | 35 + SPDY/SPDYStreamManager.m | 266 +++ SPDY/SPDYTLSTrustEvaluator.h | 16 + SPDY/SPDYZLibCommon.h | 194 ++ SPDY/en.lproj/InfoPlist.strings | 2 + SPDYUnitTests/SPDYFrameCodecTest.m | 366 ++++ SPDYUnitTests/SPDYMockFrameDecoderDelegate.h | 21 + SPDYUnitTests/SPDYMockFrameDecoderDelegate.m | 50 + SPDYUnitTests/SPDYOriginTest.m | 120 ++ SPDYUnitTests/SPDYStreamManagerTest.m | 153 ++ SPDYUnitTests/SPDYStreamTest.m | 168 ++ SPDYUnitTests/SPDYUnitTests-Info.plist | 22 + SPDYUnitTests/SPDYUnitTests-Prefix.pch | 7 + SPDYUnitTests/en.lproj/InfoPlist.strings | 2 + 63 files changed, 9518 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SPDY.iphoneos/SPDY.iphoneos-Prefix.pch create mode 100644 SPDY.iphonesimulator/SPDY.iphonesimulator-Prefix.pch create mode 100644 SPDY.macosx/SPDY.macosx-Prefix.pch create mode 100644 SPDY.xcodeproj/project.pbxproj create mode 100644 SPDY.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.iphoneos.arm64.xcscheme create mode 100644 SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.iphoneos.xcscheme create mode 100644 SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.iphonesimulator.xcscheme create mode 100644 SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.macosx.xcscheme create mode 100644 SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.xcscheme create mode 100644 SPDY.xcodeproj/xcshareddata/xcschemes/SPDYUnitTests.xcscheme create mode 100644 SPDY/NSURLRequest+SPDYURLRequest.h create mode 100644 SPDY/NSURLRequest+SPDYURLRequest.m create mode 100644 SPDY/SPDY-Info.plist create mode 100644 SPDY/SPDY-Prefix.pch create mode 100644 SPDY/SPDYCommonLogger.h create mode 100644 SPDY/SPDYCommonLogger.m create mode 100644 SPDY/SPDYDefinitions.h create mode 100644 SPDY/SPDYError.h create mode 100644 SPDY/SPDYFrame.h create mode 100644 SPDY/SPDYFrame.m create mode 100644 SPDY/SPDYFrameDecoder.h create mode 100644 SPDY/SPDYFrameDecoder.m create mode 100644 SPDY/SPDYFrameEncoder.h create mode 100644 SPDY/SPDYFrameEncoder.m create mode 100644 SPDY/SPDYHeaderBlockCompressor.h create mode 100644 SPDY/SPDYHeaderBlockCompressor.m create mode 100644 SPDY/SPDYHeaderBlockDecompressor.h create mode 100644 SPDY/SPDYHeaderBlockDecompressor.m create mode 100644 SPDY/SPDYLogger.h create mode 100644 SPDY/SPDYOrigin.h create mode 100644 SPDY/SPDYOrigin.m create mode 100644 SPDY/SPDYProtocol.h create mode 100644 SPDY/SPDYProtocol.m create mode 100644 SPDY/SPDYSession.h create mode 100644 SPDY/SPDYSession.m create mode 100644 SPDY/SPDYSessionManager.h create mode 100644 SPDY/SPDYSessionManager.m create mode 100644 SPDY/SPDYSettingsStore.h create mode 100644 SPDY/SPDYSettingsStore.m create mode 100644 SPDY/SPDYSocket.h create mode 100644 SPDY/SPDYSocket.m create mode 100644 SPDY/SPDYStream.h create mode 100644 SPDY/SPDYStream.m create mode 100644 SPDY/SPDYStreamManager.h create mode 100644 SPDY/SPDYStreamManager.m create mode 100644 SPDY/SPDYTLSTrustEvaluator.h create mode 100644 SPDY/SPDYZLibCommon.h create mode 100644 SPDY/en.lproj/InfoPlist.strings create mode 100644 SPDYUnitTests/SPDYFrameCodecTest.m create mode 100644 SPDYUnitTests/SPDYMockFrameDecoderDelegate.h create mode 100644 SPDYUnitTests/SPDYMockFrameDecoderDelegate.m create mode 100644 SPDYUnitTests/SPDYOriginTest.m create mode 100644 SPDYUnitTests/SPDYStreamManagerTest.m create mode 100644 SPDYUnitTests/SPDYStreamTest.m create mode 100644 SPDYUnitTests/SPDYUnitTests-Info.plist create mode 100644 SPDYUnitTests/SPDYUnitTests-Prefix.pch create mode 100644 SPDYUnitTests/en.lproj/InfoPlist.strings diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d4df77a --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# OS X temp files +.DS_Store +*.swp +*.lock +profile + +# AppCode stuff +.idea + +# Xcode stuff +build/ +xcuserdata +*.xccheckout diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..a682b68 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: objective-c +before_install: + - brew update +script: "xcodebuild test -workspace SPDY.xcodeproj/project.xcworkspace/ -scheme SPDYUnitTests -sdk iphonesimulator" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..91f9f1d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,68 @@ +# Contributing Guidelines + +Looking to contribute something? Here's how you can help. + +## Bug reports + +A bug is a _demonstrable problem_ that is caused by the code in the +repository. Good bug reports are extremely helpful - thank you! + +Guidelines for bug reports: + +1. **Use the GitHub issue search** - check if the issue has already been + reported. + +2. **Check if the issue has been fixed** - try to reproduce it using the + latest `master` or development branch in the repository. + +3. **Isolate the problem** - ideally create a reduced test case and a live + example. + +4. Please try to be as detailed as possible in your report. Include specific + information about the environment - operating system and version, and + steps required to reproduce. + + +## Feature requests & contribution enquiries + +Feature requests are welcome. But take a moment to find out whether your idea +fits with the scope and aims of the project. It's up to *you* to make a strong +case for the inclusion of your feature. Please provide as much detail and +context as possible. + +Contribution enquiries should take place before any significant pull request, +otherwise you risk spending a lot of time working on something that we might +have good reasons for rejecting. + +## Pull requests + +Good pull requests - patches, improvements, new features - are a fantastic +help. They should remain focused in scope and avoid containing unrelated +commits. + +Make sure to adhere to the coding conventions used throughout the codebase +(indentation, accurate comments, etc.) and any other requirements (such as test +coverage). + +Please follow this process; it's the best way to get your work included in the +project: + +1. Create a new topic branch to contain your feature, change, or fix: + +2. Commit your changes in logical chunks. Provide clear and explanatory commit + messages. Use git's [interactive rebase](https://help.github.com/articles/interactive-rebase) + feature to tidy up your commits before making them public. + +3. Locally merge (or rebase) the upstream development branch into your topic branch: + +4. Push your topic branch up to your fork: + +5. [Open a Pull Request](http://help.github.com/send-pull-requests/) with a + clear title and description. + +## License + +By contributing your code, + +You agree to license your contribution under the terms of the Apache Public License 2.0 +https://github.com/twitter/CocoaSPDY/blob/master/LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e9063eb --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# SPDY for Cocoa and Cocoa Touch +A SPDY/3.1 framework for iOS and Mac OS X [![Build Status](https://travis-ci.org/twitter/CocoaSPDY.png?branch=master)](https://travis-ci.org/twitter/CocoaSPDY) + +## The SPDY protocol +The short version is that [SPDY](http://en.wikipedia.org/wiki/SPDY) can make your HTTP requests faster. Sometimes a lot faster. For a more details, see the following: + +http://www.chromium.org/spdy/spdy-whitepaper +http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1 + +SPDY was originally designed at Google as an experimental successor to HTTP. It's a binary protocol (rather than human-readable, like HTTP), but is fully compatible with HTTP. In fact, current draft work on [HTTP/2.0](https://github.com/http2/http2-spec) is largely based on the SPDY protocol and its real-world success. + +In order to make HTTP requests go faster SPDY makes several improvements: + +The first, and arguably most important, is request multiplexing. Rather than sending one request at a time over one TCP connection, SPDY can issue many requests simultaneously over a single TCP session and handle responses in any order, as soon as they're available. + +Second, SPDY compresses both request and response headers. Headers are often nearly identical to each other across requests, generally contain lots of duplicated text, and can be quite large. This makes them an ideal candidate for compression.1 + +Finally, SPDY introduces server push.2 This can allow a server to push content that the client doesn't know it needs yet. Such content can range from additional assets like styles and images, to notifications about realtime events. + +1. Please see the note below about the CRIME attack. +2. Not currently supported in this framework, but coming soon. + +## Getting Started +The SPDY framework is designed to work seamlessly with your existing apps and projects. If you are using the NSURL stack to issue requests (or any library that provides an abstraction over it, like AFNetworking), you can simply add the SPDY framework bundle to your project, link it to your targets, and enable the protocol. + +The framework contains a multi-architecture/multi-platform ("fat") binary that supports versions of iOS 6 and above, and OS X Lion and above, as well as all hardware capable of running those operating systems. When you distribute your application, the size of the included binary will be dramatically reduced, provided you have code stripping enabled. + +## Enabling SPDY +The way you enable SPDY in your application will be slightly different depending on whether you are using NSURLConnection or NSURLSession to manage your HTTP calls. In order to cause requests issued via the NSURLConnection stack to be carried over SPDY, you'll make a method call to specify one or more origins (protocol-host-port tuple) to be handled by SPDY: + + [SPDYURLConnectionProtocol registerOrigin:@"https://api.twitter.com:443"]; + +For NSURLSession, you can configure sessions to use SPDY via NSURLSessionConfiguration: + + configuration.protocolClasses = @[[SPDYURLSessionProtocol class]]; + +You can freely use either or both methods, and existing SPDY sessions will be shared across both networking stacks. If you do use the former approach, note that registered origins will also be handled by SPDY with the default NSURLSession. + +Either of the above one-liners is all you need to do to shift HTTP requests transparently over to SPDY. Of course you still need a server that speaks SPDY! Some possibilities are: + +* [netty](http://netty.io/4.0/api/io/netty/handler/codec/spdy/package-summary.html) +* [jetty](http://www.eclipse.org/jetty/documentation/current/spdy.html) +* apache (with [mod_spdy](https://code.google.com/p/mod-spdy/)) + +## A note on NPN +Most existing SPDY implementations use a TLS extension called Next Protocol Implementation (NPN) to negotiate SPDY instead of HTTP. Unfortunately, this extension isn't supported by Secure Transport (Apple's TLS implementation), and so in order to use SPDY in your application, you'll either need to issue requests to a server that's configured to speak SPDY on a dedicated port, or use a server that's smart enough to examine the incoming request and determine whether the connection will be SPDY or HTTP based on what it looks like. At Twitter we do the latter, but the former solution may be simpler for most applications. + +In order to aid with protocol inference, this SPDY implementation includes a non-standard settings id at index 0: `SETTINGS_MINOR_VERSION`. This is necessary to differentiate between SPDY/3 and SPDY/3.1 connections that were not negotiated with NPN, since only the major version is included in the frame header. Because not all servers may support this particular setting, sending it can be disabled at runtime through protocol configuration. + +## Implementation Notes +### CRIME attack +The [CRIME attack](http://en.wikipedia.org/wiki/CRIME) is a plaintext injection technique that exploits the fact that information can be inferred from compressed content length to potentially reveal the contents of an encrypted stream. This is a serious issue for browsers, which are subject to hijacks that may allow an attacker to issue an arbitrary number of requests with known plaintext header content and observe the resulting effect on compression. + +In the context of an application that doesn't issue arbitrary requests, this is less likely to be an issue. However, before you ship a project with header compression enabled, you should understand the details of this attack and whether your application could be vulnerable. + +## Building the Framework Yourself +If you wish to compile the framework yourself, the process is fairly straightforward, and the build process should just work out of the box in Xcode. However, there are still a couple of things to note. + +Prior to Xcode 5, if you wanted to compile the framework to a dual-platform binary (as in the distribution version), you were required to set 'iOS Device' as your platform target for the framework. This was due to a quirk in the Xcode build process that would otherwise exclude some (but not all) versions of the ARM architecture from the final binary. With the release of Xcode 5, any platform target should result in the same final universal binary (the setting is essentially ignored). + +To create this binary, the build process actually depends on several static library targets and uses lipo to combine them. + +## Getting involved and Future work +We are always looking for people to get involved with the project. + +In the near future, we will be working on: + +* [Server Push](https://github.com/twitter/CocoaSPDY/issues/1) +* [Discretionary/Deferrable Request Scheduling](https://github.com/twitter/CocoaSPDY/issues/2) + +## Problems? +If you find any issues please [report them](https://github.com/twitter/CocoaSPDY/issues) or better, +send a [pull request](https://github.com/twitter/CocoaSPDY/pulls). + +## Authors +* Michael Schore +* Jeffrey Pinner + +## License +Copyright 2013 Twitter, Inc. and other contributors. + +Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 diff --git a/SPDY.iphoneos/SPDY.iphoneos-Prefix.pch b/SPDY.iphoneos/SPDY.iphoneos-Prefix.pch new file mode 100644 index 0000000..2ade4a2 --- /dev/null +++ b/SPDY.iphoneos/SPDY.iphoneos-Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'SPDY.iphoneos' target in the 'SPDY.iphoneos' project +// + +#ifdef __OBJC__ + #import +#endif diff --git a/SPDY.iphonesimulator/SPDY.iphonesimulator-Prefix.pch b/SPDY.iphonesimulator/SPDY.iphonesimulator-Prefix.pch new file mode 100644 index 0000000..49c4c06 --- /dev/null +++ b/SPDY.iphonesimulator/SPDY.iphonesimulator-Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'SPDY.iphonesimulator' target in the 'SPDY.iphonesimulator' project +// + +#ifdef __OBJC__ + #import +#endif diff --git a/SPDY.macosx/SPDY.macosx-Prefix.pch b/SPDY.macosx/SPDY.macosx-Prefix.pch new file mode 100644 index 0000000..670c2ee --- /dev/null +++ b/SPDY.macosx/SPDY.macosx-Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'SPDY.macosx' target in the 'SPDY.macosx' project +// + +#ifdef __OBJC__ + #import +#endif diff --git a/SPDY.xcodeproj/project.pbxproj b/SPDY.xcodeproj/project.pbxproj new file mode 100644 index 0000000..ecd426e --- /dev/null +++ b/SPDY.xcodeproj/project.pbxproj @@ -0,0 +1,1159 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 060C235E17CE9FCE000B4E9C /* SPDYStreamManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 060C235D17CE9FCE000B4E9C /* SPDYStreamManagerTest.m */; }; + 061C8E9517C5954400D22083 /* SPDYStreamManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 061C8E9317C5954400D22083 /* SPDYStreamManager.m */; }; + 061C8E9617C5954400D22083 /* SPDYStreamManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 061C8E9317C5954400D22083 /* SPDYStreamManager.m */; }; + 061C8E9717C5954400D22083 /* SPDYStreamManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 061C8E9317C5954400D22083 /* SPDYStreamManager.m */; }; + 061C8E9817C5954400D22083 /* SPDYStreamManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 061C8E9317C5954400D22083 /* SPDYStreamManager.m */; }; + 06290995169E4D9700E35A82 /* SPDYHeaderBlockCompressor.m in Sources */ = {isa = PBXBuildFile; fileRef = 06290993169E4D6000E35A82 /* SPDYHeaderBlockCompressor.m */; }; + 062EA640175D4CD3003BC1CE /* SPDYCommonLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 062EA63E175D4CD3003BC1CE /* SPDYCommonLogger.h */; }; + 062EA642175D4CD3003BC1CE /* SPDYCommonLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 062EA63F175D4CD3003BC1CE /* SPDYCommonLogger.m */; }; + 062EA643175D4CD3003BC1CE /* SPDYCommonLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 062EA63F175D4CD3003BC1CE /* SPDYCommonLogger.m */; }; + 062EA644175D4CD3003BC1CE /* SPDYCommonLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 062EA63F175D4CD3003BC1CE /* SPDYCommonLogger.m */; }; + 062EA645175D4CD3003BC1CE /* SPDYCommonLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 062EA63F175D4CD3003BC1CE /* SPDYCommonLogger.m */; }; + 064A05C716F7C313008C7D08 /* SPDYProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = D2CC14C01618CF62002E37CF /* SPDYProtocol.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 064A05D616F8E3AD008C7D08 /* NSURLRequest+SPDYURLRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 069D0E99168268F10037D8AF /* NSURLRequest+SPDYURLRequest.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 064EFB1516715C9F002F0AEC /* SenTestingKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 064EFB1416715C9F002F0AEC /* SenTestingKit.framework */; }; + 064EFB1816715C9F002F0AEC /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CC14B216179B43002E37CF /* Foundation.framework */; }; + 064EFB1E16715C9F002F0AEC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 064EFB1C16715C9F002F0AEC /* InfoPlist.strings */; }; + 064EFB2F1671638A002F0AEC /* SPDYMockFrameDecoderDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 064EFB2E1671638A002F0AEC /* SPDYMockFrameDecoderDelegate.m */; }; + 0651EBE916F3F7C800CE44D2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CC14B216179B43002E37CF /* Foundation.framework */; }; + 0651EBF916F3F7DC00CE44D2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CC14B216179B43002E37CF /* Foundation.framework */; }; + 0651EC0916F3F7E500CE44D2 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CC14B216179B43002E37CF /* Foundation.framework */; }; + 0651EC1E16F3F92300CE44D2 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CC14D2161B608C002E37CF /* CFNetwork.framework */; }; + 0651EC1F16F3F92500CE44D2 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 06FDA21416717F0500137DBD /* libz.dylib */; }; + 0651EC2016F3FA0B00CE44D2 /* SPDYSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14D0161A9EE9002E37CF /* SPDYSocket.m */; }; + 0651EC2216F3FA0B00CE44D2 /* NSURLRequest+SPDYURLRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E9A168268F10037D8AF /* NSURLRequest+SPDYURLRequest.m */; }; + 0651EC2316F3FA0B00CE44D2 /* SPDYFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14CA161A25CC002E37CF /* SPDYFrame.m */; }; + 0651EC2416F3FA0B00CE44D2 /* SPDYFrameDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C7161A1952002E37CF /* SPDYFrameDecoder.m */; }; + 0651EC2516F3FA0B00CE44D2 /* SPDYFrameEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E9516824A910037D8AF /* SPDYFrameEncoder.m */; }; + 0651EC2616F3FA0B00CE44D2 /* SPDYHeaderBlockCompressor.m in Sources */ = {isa = PBXBuildFile; fileRef = 06290993169E4D6000E35A82 /* SPDYHeaderBlockCompressor.m */; }; + 0651EC2716F3FA0B00CE44D2 /* SPDYHeaderBlockDecompressor.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FE895BE087AC28BBBC857F9 /* SPDYHeaderBlockDecompressor.m */; }; + 0651EC2816F3FA0B00CE44D2 /* SPDYProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C11618CF62002E37CF /* SPDYProtocol.m */; }; + 0651EC2916F3FA0B00CE44D2 /* SPDYSession.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C41618DBF2002E37CF /* SPDYSession.m */; }; + 0651EC2A16F3FA0B00CE44D2 /* SPDYSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14CD161A5826002E37CF /* SPDYSessionManager.m */; }; + 0651EC2B16F3FA0B00CE44D2 /* SPDYSettingsStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 06FC94131694B92400FC95DF /* SPDYSettingsStore.m */; }; + 0651EC2C16F3FA0B00CE44D2 /* SPDYStream.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E89167F9D010037D8AF /* SPDYStream.m */; }; + 0651EC2D16F3FA1000CE44D2 /* SPDYSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14D0161A9EE9002E37CF /* SPDYSocket.m */; }; + 0651EC2F16F3FA1000CE44D2 /* NSURLRequest+SPDYURLRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E9A168268F10037D8AF /* NSURLRequest+SPDYURLRequest.m */; }; + 0651EC3016F3FA1000CE44D2 /* SPDYFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14CA161A25CC002E37CF /* SPDYFrame.m */; }; + 0651EC3116F3FA1000CE44D2 /* SPDYFrameDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C7161A1952002E37CF /* SPDYFrameDecoder.m */; }; + 0651EC3216F3FA1000CE44D2 /* SPDYFrameEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E9516824A910037D8AF /* SPDYFrameEncoder.m */; }; + 0651EC3316F3FA1000CE44D2 /* SPDYHeaderBlockCompressor.m in Sources */ = {isa = PBXBuildFile; fileRef = 06290993169E4D6000E35A82 /* SPDYHeaderBlockCompressor.m */; }; + 0651EC3416F3FA1000CE44D2 /* SPDYHeaderBlockDecompressor.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FE895BE087AC28BBBC857F9 /* SPDYHeaderBlockDecompressor.m */; }; + 0651EC3516F3FA1000CE44D2 /* SPDYProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C11618CF62002E37CF /* SPDYProtocol.m */; }; + 0651EC3616F3FA1000CE44D2 /* SPDYSession.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C41618DBF2002E37CF /* SPDYSession.m */; }; + 0651EC3716F3FA1000CE44D2 /* SPDYSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14CD161A5826002E37CF /* SPDYSessionManager.m */; }; + 0651EC3816F3FA1000CE44D2 /* SPDYSettingsStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 06FC94131694B92400FC95DF /* SPDYSettingsStore.m */; }; + 0651EC3916F3FA1000CE44D2 /* SPDYStream.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E89167F9D010037D8AF /* SPDYStream.m */; }; + 0651EC3A16F3FA1400CE44D2 /* SPDYSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14D0161A9EE9002E37CF /* SPDYSocket.m */; }; + 0651EC3C16F3FA1400CE44D2 /* NSURLRequest+SPDYURLRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E9A168268F10037D8AF /* NSURLRequest+SPDYURLRequest.m */; }; + 0651EC3D16F3FA1400CE44D2 /* SPDYFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14CA161A25CC002E37CF /* SPDYFrame.m */; }; + 0651EC3E16F3FA1400CE44D2 /* SPDYFrameDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C7161A1952002E37CF /* SPDYFrameDecoder.m */; }; + 0651EC3F16F3FA1400CE44D2 /* SPDYFrameEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E9516824A910037D8AF /* SPDYFrameEncoder.m */; }; + 0651EC4016F3FA1400CE44D2 /* SPDYHeaderBlockCompressor.m in Sources */ = {isa = PBXBuildFile; fileRef = 06290993169E4D6000E35A82 /* SPDYHeaderBlockCompressor.m */; }; + 0651EC4116F3FA1400CE44D2 /* SPDYHeaderBlockDecompressor.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FE895BE087AC28BBBC857F9 /* SPDYHeaderBlockDecompressor.m */; }; + 0651EC4216F3FA1400CE44D2 /* SPDYProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C11618CF62002E37CF /* SPDYProtocol.m */; }; + 0651EC4316F3FA1400CE44D2 /* SPDYSession.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C41618DBF2002E37CF /* SPDYSession.m */; }; + 0651EC4416F3FA1400CE44D2 /* SPDYSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14CD161A5826002E37CF /* SPDYSessionManager.m */; }; + 0651EC4516F3FA1400CE44D2 /* SPDYSettingsStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 06FC94131694B92400FC95DF /* SPDYSettingsStore.m */; }; + 0651EC4616F3FA1400CE44D2 /* SPDYStream.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E89167F9D010037D8AF /* SPDYStream.m */; }; + 0651EC4716F3FA3A00CE44D2 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CC14D2161B608C002E37CF /* CFNetwork.framework */; }; + 0651EC4816F3FA3D00CE44D2 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 06FDA21416717F0500137DBD /* libz.dylib */; }; + 0651EC4916F3FA4300CE44D2 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CC14D2161B608C002E37CF /* CFNetwork.framework */; }; + 0651EC4A16F3FA4500CE44D2 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 06FDA21416717F0500137DBD /* libz.dylib */; }; + 0679F3CF186217FC006F122E /* SPDYOriginTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 0679F3CE186217FC006F122E /* SPDYOriginTest.m */; }; + 067EBFE717418F350029F16C /* SPDYStreamTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 067EBFE617418F350029F16C /* SPDYStreamTest.m */; }; + 06811C971714D426000D1677 /* SPDYError.h in Headers */ = {isa = PBXBuildFile; fileRef = 06811C961714D426000D1677 /* SPDYError.h */; }; + 06811C9B1715DC85000D1677 /* SPDYLogger.h in Headers */ = {isa = PBXBuildFile; fileRef = 06811C9A1715DC85000D1677 /* SPDYLogger.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 069AA03916975B65005A72CA /* SPDYFrameCodecTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 069AA03816975B65005A72CA /* SPDYFrameCodecTest.m */; }; + 069D0E8B167F9D010037D8AF /* SPDYStream.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E89167F9D010037D8AF /* SPDYStream.m */; }; + 069D0E9716824A910037D8AF /* SPDYFrameEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E9516824A910037D8AF /* SPDYFrameEncoder.m */; }; + 069D0E9C168268F10037D8AF /* NSURLRequest+SPDYURLRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E9A168268F10037D8AF /* NSURLRequest+SPDYURLRequest.m */; }; + 06A4E502180F21D500A82D93 /* SPDYSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14D0161A9EE9002E37CF /* SPDYSocket.m */; }; + 06A4E504180F21D500A82D93 /* NSURLRequest+SPDYURLRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E9A168268F10037D8AF /* NSURLRequest+SPDYURLRequest.m */; }; + 06A4E505180F21D500A82D93 /* SPDYFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14CA161A25CC002E37CF /* SPDYFrame.m */; }; + 06A4E506180F21D500A82D93 /* SPDYFrameDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C7161A1952002E37CF /* SPDYFrameDecoder.m */; }; + 06A4E507180F21D500A82D93 /* SPDYFrameEncoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E9516824A910037D8AF /* SPDYFrameEncoder.m */; }; + 06A4E508180F21D500A82D93 /* SPDYHeaderBlockCompressor.m in Sources */ = {isa = PBXBuildFile; fileRef = 06290993169E4D6000E35A82 /* SPDYHeaderBlockCompressor.m */; }; + 06A4E509180F21D500A82D93 /* SPDYHeaderBlockDecompressor.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FE895BE087AC28BBBC857F9 /* SPDYHeaderBlockDecompressor.m */; }; + 06A4E50A180F21D500A82D93 /* SPDYProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C11618CF62002E37CF /* SPDYProtocol.m */; }; + 06A4E50B180F21D500A82D93 /* SPDYSession.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C41618DBF2002E37CF /* SPDYSession.m */; }; + 06A4E50C180F21D500A82D93 /* SPDYSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14CD161A5826002E37CF /* SPDYSessionManager.m */; }; + 06A4E50D180F21D500A82D93 /* SPDYSettingsStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 06FC94131694B92400FC95DF /* SPDYSettingsStore.m */; }; + 06A4E50E180F21D500A82D93 /* SPDYStream.m in Sources */ = {isa = PBXBuildFile; fileRef = 069D0E89167F9D010037D8AF /* SPDYStream.m */; }; + 06A4E50F180F21D500A82D93 /* SPDYCommonLogger.m in Sources */ = {isa = PBXBuildFile; fileRef = 062EA63F175D4CD3003BC1CE /* SPDYCommonLogger.m */; }; + 06A4E510180F21D500A82D93 /* SPDYStreamManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 061C8E9317C5954400D22083 /* SPDYStreamManager.m */; }; + 06A4E512180F21D500A82D93 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CC14B216179B43002E37CF /* Foundation.framework */; }; + 06A4E513180F21D500A82D93 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CC14D2161B608C002E37CF /* CFNetwork.framework */; }; + 06A4E514180F21D500A82D93 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 06FDA21416717F0500137DBD /* libz.dylib */; }; + 06B290CE1861018900540A03 /* SPDYOrigin.m in Sources */ = {isa = PBXBuildFile; fileRef = 06B290CD1861018900540A03 /* SPDYOrigin.m */; }; + 06B290CF1861018A00540A03 /* SPDYOrigin.m in Sources */ = {isa = PBXBuildFile; fileRef = 06B290CD1861018900540A03 /* SPDYOrigin.m */; }; + 06B290D01861018A00540A03 /* SPDYOrigin.m in Sources */ = {isa = PBXBuildFile; fileRef = 06B290CD1861018900540A03 /* SPDYOrigin.m */; }; + 06B290D11861018A00540A03 /* SPDYOrigin.m in Sources */ = {isa = PBXBuildFile; fileRef = 06B290CD1861018900540A03 /* SPDYOrigin.m */; }; + 06B290D21861018A00540A03 /* SPDYOrigin.m in Sources */ = {isa = PBXBuildFile; fileRef = 06B290CD1861018900540A03 /* SPDYOrigin.m */; }; + 06E7BF131824371F004DB65D /* SPDYTLSTrustEvaluator.h in Headers */ = {isa = PBXBuildFile; fileRef = 06E7BF111823B74D004DB65D /* SPDYTLSTrustEvaluator.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 06FC94151694B92400FC95DF /* SPDYSettingsStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 06FC94131694B92400FC95DF /* SPDYSettingsStore.m */; }; + 06FDA20616717DF100137DBD /* SPDYSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14D0161A9EE9002E37CF /* SPDYSocket.m */; }; + 06FDA20916717DF100137DBD /* SPDYFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14CA161A25CC002E37CF /* SPDYFrame.m */; }; + 06FDA20B16717DF100137DBD /* SPDYFrameDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C7161A1952002E37CF /* SPDYFrameDecoder.m */; }; + 06FDA20D16717DF100137DBD /* SPDYProtocol.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C11618CF62002E37CF /* SPDYProtocol.m */; }; + 06FDA20F16717DF100137DBD /* SPDYSession.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14C41618DBF2002E37CF /* SPDYSession.m */; }; + 06FDA21116717DF100137DBD /* SPDYSessionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = D2CC14CD161A5826002E37CF /* SPDYSessionManager.m */; }; + 06FDA21216717DF100137DBD /* SPDYHeaderBlockDecompressor.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FE895BE087AC28BBBC857F9 /* SPDYHeaderBlockDecompressor.m */; }; + 06FDA21616717F1800137DBD /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 06FDA21416717F0500137DBD /* libz.dylib */; }; + 06FDA21716717F5E00137DBD /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D2CC14D2161B608C002E37CF /* CFNetwork.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 064A05C116F7C2F3008C7D08 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D2CC14A516179B43002E37CF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0651EBE716F3F7C700CE44D2; + remoteInfo = SPDY.iphoneos; + }; + 064A05C316F7C2F5008C7D08 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D2CC14A516179B43002E37CF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0651EBF716F3F7DB00CE44D2; + remoteInfo = SPDY.iphonesimulator; + }; + 064A05C516F7C2F6008C7D08 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D2CC14A516179B43002E37CF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0651EC0716F3F7E500CE44D2; + remoteInfo = SPDY.macosx; + }; + 06D032B4180F2BB500C489A1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D2CC14A516179B43002E37CF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 06A4E500180F21D500A82D93; + remoteInfo = SPDY.iphoneos.arm64; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 060C235D17CE9FCE000B4E9C /* SPDYStreamManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYStreamManagerTest.m; sourceTree = ""; }; + 061C8E9217C5954400D22083 /* SPDYStreamManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYStreamManager.h; sourceTree = ""; }; + 061C8E9317C5954400D22083 /* SPDYStreamManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYStreamManager.m; sourceTree = ""; }; + 06290990169E497300E35A82 /* SPDYZLibCommon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYZLibCommon.h; sourceTree = ""; }; + 06290992169E4D5F00E35A82 /* SPDYHeaderBlockCompressor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYHeaderBlockCompressor.h; sourceTree = ""; }; + 06290993169E4D6000E35A82 /* SPDYHeaderBlockCompressor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYHeaderBlockCompressor.m; sourceTree = ""; }; + 062EA63C175D1A15003BC1CE /* SPDYDefinitions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYDefinitions.h; sourceTree = ""; }; + 062EA63E175D4CD3003BC1CE /* SPDYCommonLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYCommonLogger.h; sourceTree = ""; }; + 062EA63F175D4CD3003BC1CE /* SPDYCommonLogger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYCommonLogger.m; sourceTree = ""; }; + 06336DE216F2756A00532180 /* SPDY-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SPDY-Info.plist"; sourceTree = ""; }; + 06336DE416F2756A00532180 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 06336DE616F2756A00532180 /* SPDY-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SPDY-Prefix.pch"; sourceTree = ""; }; + 064EFB1316715C9F002F0AEC /* SPDYUnitTests.octest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SPDYUnitTests.octest; sourceTree = BUILT_PRODUCTS_DIR; }; + 064EFB1416715C9F002F0AEC /* SenTestingKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SenTestingKit.framework; path = Library/Frameworks/SenTestingKit.framework; sourceTree = DEVELOPER_DIR; }; + 064EFB1B16715C9F002F0AEC /* SPDYUnitTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "SPDYUnitTests-Info.plist"; sourceTree = ""; }; + 064EFB1D16715C9F002F0AEC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + 064EFB2216715C9F002F0AEC /* SPDYUnitTests-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SPDYUnitTests-Prefix.pch"; sourceTree = ""; }; + 064EFB2D16716389002F0AEC /* SPDYMockFrameDecoderDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYMockFrameDecoderDelegate.h; sourceTree = ""; }; + 064EFB2E1671638A002F0AEC /* SPDYMockFrameDecoderDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYMockFrameDecoderDelegate.m; sourceTree = ""; }; + 0651EBE816F3F7C700CE44D2 /* libSPDY.iphoneos.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSPDY.iphoneos.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 0651EBEC16F3F7C800CE44D2 /* SPDY.iphoneos-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SPDY.iphoneos-Prefix.pch"; sourceTree = ""; }; + 0651EBF816F3F7DB00CE44D2 /* libSPDY.iphonesimulator.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSPDY.iphonesimulator.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 0651EBFC16F3F7DC00CE44D2 /* SPDY.iphonesimulator-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SPDY.iphonesimulator-Prefix.pch"; sourceTree = ""; }; + 0651EC0816F3F7E500CE44D2 /* libSPDY.macosx.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSPDY.macosx.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 0651EC0C16F3F7E500CE44D2 /* SPDY.macosx-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SPDY.macosx-Prefix.pch"; sourceTree = ""; }; + 0652631316F7B6360081868F /* SPDY.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SPDY.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0679F3CE186217FC006F122E /* SPDYOriginTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYOriginTest.m; sourceTree = ""; }; + 067EBFE617418F350029F16C /* SPDYStreamTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYStreamTest.m; sourceTree = ""; }; + 06811C961714D426000D1677 /* SPDYError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYError.h; sourceTree = ""; }; + 06811C9A1715DC85000D1677 /* SPDYLogger.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYLogger.h; sourceTree = ""; }; + 069AA03816975B65005A72CA /* SPDYFrameCodecTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYFrameCodecTest.m; sourceTree = ""; }; + 069D0E88167F9D010037D8AF /* SPDYStream.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYStream.h; sourceTree = ""; }; + 069D0E89167F9D010037D8AF /* SPDYStream.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYStream.m; sourceTree = ""; }; + 069D0E9416824A910037D8AF /* SPDYFrameEncoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYFrameEncoder.h; sourceTree = ""; }; + 069D0E9516824A910037D8AF /* SPDYFrameEncoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYFrameEncoder.m; sourceTree = ""; }; + 069D0E99168268F10037D8AF /* NSURLRequest+SPDYURLRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURLRequest+SPDYURLRequest.h"; sourceTree = ""; }; + 069D0E9A168268F10037D8AF /* NSURLRequest+SPDYURLRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSURLRequest+SPDYURLRequest.m"; sourceTree = ""; }; + 06A4E518180F21D500A82D93 /* libSPDY.iphoneos.arm64.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSPDY.iphoneos.arm64.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 06B290CC1861018900540A03 /* SPDYOrigin.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYOrigin.h; sourceTree = ""; }; + 06B290CD1861018900540A03 /* SPDYOrigin.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYOrigin.m; sourceTree = ""; }; + 06E7BF111823B74D004DB65D /* SPDYTLSTrustEvaluator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYTLSTrustEvaluator.h; sourceTree = ""; }; + 06FC94121694B92400FC95DF /* SPDYSettingsStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYSettingsStore.h; sourceTree = ""; }; + 06FC94131694B92400FC95DF /* SPDYSettingsStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYSettingsStore.m; sourceTree = ""; }; + 06FDA21416717F0500137DBD /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; }; + 4FE891C7065B348CC7EF4BFC /* SPDYHeaderBlockDecompressor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYHeaderBlockDecompressor.h; sourceTree = ""; }; + 4FE895BE087AC28BBBC857F9 /* SPDYHeaderBlockDecompressor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYHeaderBlockDecompressor.m; sourceTree = ""; }; + D2CC14B216179B43002E37CF /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; + D2CC14B816179B43002E37CF /* SPDY-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SPDY-Prefix.pch"; sourceTree = ""; }; + D2CC14C01618CF62002E37CF /* SPDYProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYProtocol.h; sourceTree = ""; }; + D2CC14C11618CF62002E37CF /* SPDYProtocol.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYProtocol.m; sourceTree = ""; }; + D2CC14C31618DBF2002E37CF /* SPDYSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYSession.h; sourceTree = ""; }; + D2CC14C41618DBF2002E37CF /* SPDYSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYSession.m; sourceTree = ""; }; + D2CC14C6161A1952002E37CF /* SPDYFrameDecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYFrameDecoder.h; sourceTree = ""; }; + D2CC14C7161A1952002E37CF /* SPDYFrameDecoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYFrameDecoder.m; sourceTree = ""; }; + D2CC14C9161A25CC002E37CF /* SPDYFrame.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYFrame.h; sourceTree = ""; }; + D2CC14CA161A25CC002E37CF /* SPDYFrame.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYFrame.m; sourceTree = ""; }; + D2CC14CC161A5826002E37CF /* SPDYSessionManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYSessionManager.h; sourceTree = ""; }; + D2CC14CD161A5826002E37CF /* SPDYSessionManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYSessionManager.m; sourceTree = ""; }; + D2CC14CF161A9EE9002E37CF /* SPDYSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYSocket.h; sourceTree = ""; }; + D2CC14D0161A9EE9002E37CF /* SPDYSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYSocket.m; sourceTree = ""; }; + D2CC14D2161B608C002E37CF /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = System/Library/Frameworks/CFNetwork.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 064EFB0F16715C9F002F0AEC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 06FDA21616717F1800137DBD /* libz.dylib in Frameworks */, + 06FDA21716717F5E00137DBD /* CFNetwork.framework in Frameworks */, + 064EFB1816715C9F002F0AEC /* Foundation.framework in Frameworks */, + 064EFB1516715C9F002F0AEC /* SenTestingKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0651EBE516F3F7C700CE44D2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0651EBE916F3F7C800CE44D2 /* Foundation.framework in Frameworks */, + 0651EC4916F3FA4300CE44D2 /* CFNetwork.framework in Frameworks */, + 0651EC4A16F3FA4500CE44D2 /* libz.dylib in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0651EBF516F3F7DB00CE44D2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0651EBF916F3F7DC00CE44D2 /* Foundation.framework in Frameworks */, + 0651EC4716F3FA3A00CE44D2 /* CFNetwork.framework in Frameworks */, + 0651EC4816F3FA3D00CE44D2 /* libz.dylib in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0651EC0516F3F7E500CE44D2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0651EC0916F3F7E500CE44D2 /* Foundation.framework in Frameworks */, + 0651EC1E16F3F92300CE44D2 /* CFNetwork.framework in Frameworks */, + 0651EC1F16F3F92500CE44D2 /* libz.dylib in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 06A4E511180F21D500A82D93 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 06A4E512180F21D500A82D93 /* Foundation.framework in Frameworks */, + 06A4E513180F21D500A82D93 /* CFNetwork.framework in Frameworks */, + 06A4E514180F21D500A82D93 /* libz.dylib in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 06336DE016F2756A00532180 /* SPDY.framework */ = { + isa = PBXGroup; + children = ( + 06336DE316F2756A00532180 /* InfoPlist.strings */, + 06336DE216F2756A00532180 /* SPDY-Info.plist */, + 06336DE616F2756A00532180 /* SPDY-Prefix.pch */, + ); + name = SPDY.framework; + path = SPDY; + sourceTree = ""; + }; + 064EFB1916715C9F002F0AEC /* SPDYUnitTests */ = { + isa = PBXGroup; + children = ( + 069AA03816975B65005A72CA /* SPDYFrameCodecTest.m */, + 0679F3CE186217FC006F122E /* SPDYOriginTest.m */, + 060C235D17CE9FCE000B4E9C /* SPDYStreamManagerTest.m */, + 067EBFE617418F350029F16C /* SPDYStreamTest.m */, + 064EFB1A16715C9F002F0AEC /* Supporting Files */, + ); + path = SPDYUnitTests; + sourceTree = ""; + }; + 064EFB1A16715C9F002F0AEC /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 064EFB1B16715C9F002F0AEC /* SPDYUnitTests-Info.plist */, + 064EFB1C16715C9F002F0AEC /* InfoPlist.strings */, + 064EFB2216715C9F002F0AEC /* SPDYUnitTests-Prefix.pch */, + 064EFB2D16716389002F0AEC /* SPDYMockFrameDecoderDelegate.h */, + 064EFB2E1671638A002F0AEC /* SPDYMockFrameDecoderDelegate.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 0651EBEA16F3F7C800CE44D2 /* SPDY.iphoneos */ = { + isa = PBXGroup; + children = ( + 0651EBEC16F3F7C800CE44D2 /* SPDY.iphoneos-Prefix.pch */, + ); + path = SPDY.iphoneos; + sourceTree = ""; + }; + 0651EBFA16F3F7DC00CE44D2 /* SPDY.iphonesimulator */ = { + isa = PBXGroup; + children = ( + 0651EBFC16F3F7DC00CE44D2 /* SPDY.iphonesimulator-Prefix.pch */, + ); + path = SPDY.iphonesimulator; + sourceTree = ""; + }; + 0651EC0A16F3F7E500CE44D2 /* SPDY.macosx */ = { + isa = PBXGroup; + children = ( + 0651EC0C16F3F7E500CE44D2 /* SPDY.macosx-Prefix.pch */, + ); + path = SPDY.macosx; + sourceTree = ""; + }; + D2CC14A316179B43002E37CF = { + isa = PBXGroup; + children = ( + D2CC14B416179B43002E37CF /* SPDY */, + 064EFB1916715C9F002F0AEC /* SPDYUnitTests */, + 06336DE016F2756A00532180 /* SPDY.framework */, + 0651EBEA16F3F7C800CE44D2 /* SPDY.iphoneos */, + 0651EBFA16F3F7DC00CE44D2 /* SPDY.iphonesimulator */, + 0651EC0A16F3F7E500CE44D2 /* SPDY.macosx */, + D2CC14B116179B43002E37CF /* Frameworks */, + D2CC14AF16179B43002E37CF /* Products */, + ); + sourceTree = ""; + }; + D2CC14AF16179B43002E37CF /* Products */ = { + isa = PBXGroup; + children = ( + 064EFB1316715C9F002F0AEC /* SPDYUnitTests.octest */, + 0651EBE816F3F7C700CE44D2 /* libSPDY.iphoneos.a */, + 06A4E518180F21D500A82D93 /* libSPDY.iphoneos.arm64.a */, + 0651EBF816F3F7DB00CE44D2 /* libSPDY.iphonesimulator.a */, + 0651EC0816F3F7E500CE44D2 /* libSPDY.macosx.a */, + 0652631316F7B6360081868F /* SPDY.framework */, + ); + name = Products; + sourceTree = ""; + }; + D2CC14B116179B43002E37CF /* Frameworks */ = { + isa = PBXGroup; + children = ( + D2CC14D2161B608C002E37CF /* CFNetwork.framework */, + D2CC14B216179B43002E37CF /* Foundation.framework */, + 064EFB1416715C9F002F0AEC /* SenTestingKit.framework */, + 06FDA21416717F0500137DBD /* libz.dylib */, + ); + name = Frameworks; + sourceTree = ""; + }; + D2CC14B416179B43002E37CF /* SPDY */ = { + isa = PBXGroup; + children = ( + D2CC14CF161A9EE9002E37CF /* SPDYSocket.h */, + D2CC14D0161A9EE9002E37CF /* SPDYSocket.m */, + 069D0E99168268F10037D8AF /* NSURLRequest+SPDYURLRequest.h */, + 069D0E9A168268F10037D8AF /* NSURLRequest+SPDYURLRequest.m */, + D2CC14B816179B43002E37CF /* SPDY-Prefix.pch */, + 06811C961714D426000D1677 /* SPDYError.h */, + D2CC14C9161A25CC002E37CF /* SPDYFrame.h */, + D2CC14CA161A25CC002E37CF /* SPDYFrame.m */, + D2CC14C6161A1952002E37CF /* SPDYFrameDecoder.h */, + D2CC14C7161A1952002E37CF /* SPDYFrameDecoder.m */, + 069D0E9416824A910037D8AF /* SPDYFrameEncoder.h */, + 069D0E9516824A910037D8AF /* SPDYFrameEncoder.m */, + 06290992169E4D5F00E35A82 /* SPDYHeaderBlockCompressor.h */, + 06290993169E4D6000E35A82 /* SPDYHeaderBlockCompressor.m */, + 4FE891C7065B348CC7EF4BFC /* SPDYHeaderBlockDecompressor.h */, + 4FE895BE087AC28BBBC857F9 /* SPDYHeaderBlockDecompressor.m */, + 06811C9A1715DC85000D1677 /* SPDYLogger.h */, + D2CC14C01618CF62002E37CF /* SPDYProtocol.h */, + D2CC14C11618CF62002E37CF /* SPDYProtocol.m */, + D2CC14C31618DBF2002E37CF /* SPDYSession.h */, + D2CC14C41618DBF2002E37CF /* SPDYSession.m */, + D2CC14CC161A5826002E37CF /* SPDYSessionManager.h */, + D2CC14CD161A5826002E37CF /* SPDYSessionManager.m */, + 06FC94121694B92400FC95DF /* SPDYSettingsStore.h */, + 06FC94131694B92400FC95DF /* SPDYSettingsStore.m */, + 069D0E88167F9D010037D8AF /* SPDYStream.h */, + 069D0E89167F9D010037D8AF /* SPDYStream.m */, + 06290990169E497300E35A82 /* SPDYZLibCommon.h */, + 062EA63C175D1A15003BC1CE /* SPDYDefinitions.h */, + 062EA63E175D4CD3003BC1CE /* SPDYCommonLogger.h */, + 062EA63F175D4CD3003BC1CE /* SPDYCommonLogger.m */, + 061C8E9217C5954400D22083 /* SPDYStreamManager.h */, + 061C8E9317C5954400D22083 /* SPDYStreamManager.m */, + 06E7BF111823B74D004DB65D /* SPDYTLSTrustEvaluator.h */, + 06B290CC1861018900540A03 /* SPDYOrigin.h */, + 06B290CD1861018900540A03 /* SPDYOrigin.m */, + ); + path = SPDY; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 0652631016F7B6360081868F /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 064A05D616F8E3AD008C7D08 /* NSURLRequest+SPDYURLRequest.h in Headers */, + 06811C9B1715DC85000D1677 /* SPDYLogger.h in Headers */, + 064A05C716F7C313008C7D08 /* SPDYProtocol.h in Headers */, + 06E7BF131824371F004DB65D /* SPDYTLSTrustEvaluator.h in Headers */, + 06811C971714D426000D1677 /* SPDYError.h in Headers */, + 062EA640175D4CD3003BC1CE /* SPDYCommonLogger.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 064EFB1216715C9F002F0AEC /* SPDYUnitTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 064EFB2316715C9F002F0AEC /* Build configuration list for PBXNativeTarget "SPDYUnitTests" */; + buildPhases = ( + 064EFB0E16715C9F002F0AEC /* Sources */, + 064EFB0F16715C9F002F0AEC /* Frameworks */, + 064EFB1016715C9F002F0AEC /* Resources */, + 064EFB1116715C9F002F0AEC /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SPDYUnitTests; + productName = SPDYUnitTests; + productReference = 064EFB1316715C9F002F0AEC /* SPDYUnitTests.octest */; + productType = "com.apple.product-type.bundle"; + }; + 0651EBE716F3F7C700CE44D2 /* SPDY.iphoneos */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0651EBF316F3F7C800CE44D2 /* Build configuration list for PBXNativeTarget "SPDY.iphoneos" */; + buildPhases = ( + 0651EBE416F3F7C700CE44D2 /* Sources */, + 0651EBE516F3F7C700CE44D2 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SPDY.iphoneos; + productName = SPDY.iphoneos; + productReference = 0651EBE816F3F7C700CE44D2 /* libSPDY.iphoneos.a */; + productType = "com.apple.product-type.library.static"; + }; + 0651EBF716F3F7DB00CE44D2 /* SPDY.iphonesimulator */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0651EC0116F3F7DC00CE44D2 /* Build configuration list for PBXNativeTarget "SPDY.iphonesimulator" */; + buildPhases = ( + 0651EBF416F3F7DB00CE44D2 /* Sources */, + 0651EBF516F3F7DB00CE44D2 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SPDY.iphonesimulator; + productName = SPDY.iphonesimulator; + productReference = 0651EBF816F3F7DB00CE44D2 /* libSPDY.iphonesimulator.a */; + productType = "com.apple.product-type.library.static"; + }; + 0651EC0716F3F7E500CE44D2 /* SPDY.macosx */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0651EC1116F3F7E500CE44D2 /* Build configuration list for PBXNativeTarget "SPDY.macosx" */; + buildPhases = ( + 0651EC0416F3F7E500CE44D2 /* Sources */, + 0651EC0516F3F7E500CE44D2 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SPDY.macosx; + productName = SPDY.macosx; + productReference = 0651EC0816F3F7E500CE44D2 /* libSPDY.macosx.a */; + productType = "com.apple.product-type.library.static"; + }; + 0652631216F7B6360081868F /* SPDY */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0652632416F7B6360081868F /* Build configuration list for PBXNativeTarget "SPDY" */; + buildPhases = ( + 0652631016F7B6360081868F /* Headers */, + 0652631116F7B6360081868F /* Resources */, + 064A05C816F7C3B2008C7D08 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 064A05C216F7C2F3008C7D08 /* PBXTargetDependency */, + 06D032B5180F2BB500C489A1 /* PBXTargetDependency */, + 064A05C416F7C2F5008C7D08 /* PBXTargetDependency */, + 064A05C616F7C2F6008C7D08 /* PBXTargetDependency */, + ); + name = SPDY; + productName = SPDY; + productReference = 0652631316F7B6360081868F /* SPDY.framework */; + productType = "com.apple.product-type.framework"; + }; + 06A4E500180F21D500A82D93 /* SPDY.iphoneos.arm64 */ = { + isa = PBXNativeTarget; + buildConfigurationList = 06A4E515180F21D500A82D93 /* Build configuration list for PBXNativeTarget "SPDY.iphoneos.arm64" */; + buildPhases = ( + 06A4E501180F21D500A82D93 /* Sources */, + 06A4E511180F21D500A82D93 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SPDY.iphoneos.arm64; + productName = SPDY.iphoneos.arm64; + productReference = 06A4E518180F21D500A82D93 /* libSPDY.iphoneos.arm64.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + D2CC14A516179B43002E37CF /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0500; + ORGANIZATIONNAME = Twitter; + }; + buildConfigurationList = D2CC14A816179B43002E37CF /* Build configuration list for PBXProject "SPDY" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = D2CC14A316179B43002E37CF; + productRefGroup = D2CC14AF16179B43002E37CF /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 064EFB1216715C9F002F0AEC /* SPDYUnitTests */, + 0651EBE716F3F7C700CE44D2 /* SPDY.iphoneos */, + 06A4E500180F21D500A82D93 /* SPDY.iphoneos.arm64 */, + 0651EBF716F3F7DB00CE44D2 /* SPDY.iphonesimulator */, + 0651EC0716F3F7E500CE44D2 /* SPDY.macosx */, + 0652631216F7B6360081868F /* SPDY */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 064EFB1016715C9F002F0AEC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 064EFB1E16715C9F002F0AEC /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0652631116F7B6360081868F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 064A05C816F7C3B2008C7D08 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "IOS_ARMV7_LIB_PATH=\"${BUILD_DIR}/${CONFIGURATION}/lib${PROJECT_NAME}.iphoneos.a\" &&\nIOS_ARM64_LIB_PATH=\"${BUILD_DIR}/${CONFIGURATION}/lib${PROJECT_NAME}.iphoneos.arm64.a\" &&\nIOS_SIM_LIB_PATH=\"${BUILD_DIR}/${CONFIGURATION}/lib${PROJECT_NAME}.iphonesimulator.a\" &&\nOSX_LIB_PATH=\"${BUILD_DIR}/${CONFIGURATION}/lib${PROJECT_NAME}.macosx.a\" &&\nFRAMEWORK_PATH=\"${BUILD_DIR}/${CONFIGURATION}/${PRODUCT_NAME}.framework\" &&\n\nlipo \"${IOS_ARMV7_LIB_PATH}\" \"${IOS_ARM64_LIB_PATH}\" \"${IOS_SIM_LIB_PATH}\" \"${OSX_LIB_PATH}\" -create -output \"${FRAMEWORK_PATH}/Versions/A/${PRODUCT_NAME}\" &&\nln -fs \"Versions/Current/${PRODUCT_NAME}\" \"${FRAMEWORK_PATH}/${PRODUCT_NAME}\"\n"; + }; + 064EFB1116715C9F002F0AEC /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Run the unit tests in this test bundle.\n\"${SYSTEM_DEVELOPER_DIR}/Tools/RunUnitTests\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 064EFB0E16715C9F002F0AEC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 06FDA20616717DF100137DBD /* SPDYSocket.m in Sources */, + 06FDA20916717DF100137DBD /* SPDYFrame.m in Sources */, + 06FDA20B16717DF100137DBD /* SPDYFrameDecoder.m in Sources */, + 0679F3CF186217FC006F122E /* SPDYOriginTest.m in Sources */, + 06FDA20D16717DF100137DBD /* SPDYProtocol.m in Sources */, + 06FDA20F16717DF100137DBD /* SPDYSession.m in Sources */, + 06FDA21116717DF100137DBD /* SPDYSessionManager.m in Sources */, + 060C235E17CE9FCE000B4E9C /* SPDYStreamManagerTest.m in Sources */, + 06290995169E4D9700E35A82 /* SPDYHeaderBlockCompressor.m in Sources */, + 06FDA21216717DF100137DBD /* SPDYHeaderBlockDecompressor.m in Sources */, + 064EFB2F1671638A002F0AEC /* SPDYMockFrameDecoderDelegate.m in Sources */, + 069D0E8B167F9D010037D8AF /* SPDYStream.m in Sources */, + 069D0E9716824A910037D8AF /* SPDYFrameEncoder.m in Sources */, + 069D0E9C168268F10037D8AF /* NSURLRequest+SPDYURLRequest.m in Sources */, + 06FC94151694B92400FC95DF /* SPDYSettingsStore.m in Sources */, + 069AA03916975B65005A72CA /* SPDYFrameCodecTest.m in Sources */, + 067EBFE717418F350029F16C /* SPDYStreamTest.m in Sources */, + 062EA642175D4CD3003BC1CE /* SPDYCommonLogger.m in Sources */, + 061C8E9517C5954400D22083 /* SPDYStreamManager.m in Sources */, + 06B290CE1861018900540A03 /* SPDYOrigin.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0651EBE416F3F7C700CE44D2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0651EC3A16F3FA1400CE44D2 /* SPDYSocket.m in Sources */, + 0651EC3C16F3FA1400CE44D2 /* NSURLRequest+SPDYURLRequest.m in Sources */, + 0651EC3D16F3FA1400CE44D2 /* SPDYFrame.m in Sources */, + 0651EC3E16F3FA1400CE44D2 /* SPDYFrameDecoder.m in Sources */, + 0651EC3F16F3FA1400CE44D2 /* SPDYFrameEncoder.m in Sources */, + 0651EC4016F3FA1400CE44D2 /* SPDYHeaderBlockCompressor.m in Sources */, + 0651EC4116F3FA1400CE44D2 /* SPDYHeaderBlockDecompressor.m in Sources */, + 0651EC4216F3FA1400CE44D2 /* SPDYProtocol.m in Sources */, + 0651EC4316F3FA1400CE44D2 /* SPDYSession.m in Sources */, + 0651EC4416F3FA1400CE44D2 /* SPDYSessionManager.m in Sources */, + 0651EC4516F3FA1400CE44D2 /* SPDYSettingsStore.m in Sources */, + 0651EC4616F3FA1400CE44D2 /* SPDYStream.m in Sources */, + 062EA643175D4CD3003BC1CE /* SPDYCommonLogger.m in Sources */, + 061C8E9617C5954400D22083 /* SPDYStreamManager.m in Sources */, + 06B290CF1861018A00540A03 /* SPDYOrigin.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0651EBF416F3F7DB00CE44D2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0651EC2D16F3FA1000CE44D2 /* SPDYSocket.m in Sources */, + 0651EC2F16F3FA1000CE44D2 /* NSURLRequest+SPDYURLRequest.m in Sources */, + 0651EC3016F3FA1000CE44D2 /* SPDYFrame.m in Sources */, + 0651EC3116F3FA1000CE44D2 /* SPDYFrameDecoder.m in Sources */, + 0651EC3216F3FA1000CE44D2 /* SPDYFrameEncoder.m in Sources */, + 0651EC3316F3FA1000CE44D2 /* SPDYHeaderBlockCompressor.m in Sources */, + 0651EC3416F3FA1000CE44D2 /* SPDYHeaderBlockDecompressor.m in Sources */, + 0651EC3516F3FA1000CE44D2 /* SPDYProtocol.m in Sources */, + 0651EC3616F3FA1000CE44D2 /* SPDYSession.m in Sources */, + 0651EC3716F3FA1000CE44D2 /* SPDYSessionManager.m in Sources */, + 0651EC3816F3FA1000CE44D2 /* SPDYSettingsStore.m in Sources */, + 0651EC3916F3FA1000CE44D2 /* SPDYStream.m in Sources */, + 062EA644175D4CD3003BC1CE /* SPDYCommonLogger.m in Sources */, + 061C8E9717C5954400D22083 /* SPDYStreamManager.m in Sources */, + 06B290D11861018A00540A03 /* SPDYOrigin.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0651EC0416F3F7E500CE44D2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0651EC2016F3FA0B00CE44D2 /* SPDYSocket.m in Sources */, + 0651EC2216F3FA0B00CE44D2 /* NSURLRequest+SPDYURLRequest.m in Sources */, + 0651EC2316F3FA0B00CE44D2 /* SPDYFrame.m in Sources */, + 0651EC2416F3FA0B00CE44D2 /* SPDYFrameDecoder.m in Sources */, + 0651EC2516F3FA0B00CE44D2 /* SPDYFrameEncoder.m in Sources */, + 0651EC2616F3FA0B00CE44D2 /* SPDYHeaderBlockCompressor.m in Sources */, + 0651EC2716F3FA0B00CE44D2 /* SPDYHeaderBlockDecompressor.m in Sources */, + 0651EC2816F3FA0B00CE44D2 /* SPDYProtocol.m in Sources */, + 0651EC2916F3FA0B00CE44D2 /* SPDYSession.m in Sources */, + 0651EC2A16F3FA0B00CE44D2 /* SPDYSessionManager.m in Sources */, + 0651EC2B16F3FA0B00CE44D2 /* SPDYSettingsStore.m in Sources */, + 0651EC2C16F3FA0B00CE44D2 /* SPDYStream.m in Sources */, + 062EA645175D4CD3003BC1CE /* SPDYCommonLogger.m in Sources */, + 061C8E9817C5954400D22083 /* SPDYStreamManager.m in Sources */, + 06B290D21861018A00540A03 /* SPDYOrigin.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 06A4E501180F21D500A82D93 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 06A4E502180F21D500A82D93 /* SPDYSocket.m in Sources */, + 06A4E504180F21D500A82D93 /* NSURLRequest+SPDYURLRequest.m in Sources */, + 06A4E505180F21D500A82D93 /* SPDYFrame.m in Sources */, + 06A4E506180F21D500A82D93 /* SPDYFrameDecoder.m in Sources */, + 06A4E507180F21D500A82D93 /* SPDYFrameEncoder.m in Sources */, + 06A4E508180F21D500A82D93 /* SPDYHeaderBlockCompressor.m in Sources */, + 06A4E509180F21D500A82D93 /* SPDYHeaderBlockDecompressor.m in Sources */, + 06A4E50A180F21D500A82D93 /* SPDYProtocol.m in Sources */, + 06A4E50B180F21D500A82D93 /* SPDYSession.m in Sources */, + 06A4E50C180F21D500A82D93 /* SPDYSessionManager.m in Sources */, + 06A4E50D180F21D500A82D93 /* SPDYSettingsStore.m in Sources */, + 06A4E50E180F21D500A82D93 /* SPDYStream.m in Sources */, + 06A4E50F180F21D500A82D93 /* SPDYCommonLogger.m in Sources */, + 06A4E510180F21D500A82D93 /* SPDYStreamManager.m in Sources */, + 06B290D01861018A00540A03 /* SPDYOrigin.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 064A05C216F7C2F3008C7D08 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0651EBE716F3F7C700CE44D2 /* SPDY.iphoneos */; + targetProxy = 064A05C116F7C2F3008C7D08 /* PBXContainerItemProxy */; + }; + 064A05C416F7C2F5008C7D08 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0651EBF716F3F7DB00CE44D2 /* SPDY.iphonesimulator */; + targetProxy = 064A05C316F7C2F5008C7D08 /* PBXContainerItemProxy */; + }; + 064A05C616F7C2F6008C7D08 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0651EC0716F3F7E500CE44D2 /* SPDY.macosx */; + targetProxy = 064A05C516F7C2F6008C7D08 /* PBXContainerItemProxy */; + }; + 06D032B5180F2BB500C489A1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 06A4E500180F21D500A82D93 /* SPDY.iphoneos.arm64 */; + targetProxy = 06D032B4180F2BB500C489A1 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 06336DE316F2756A00532180 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 06336DE416F2756A00532180 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; + 064EFB1C16715C9F002F0AEC /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + 064EFB1D16715C9F002F0AEC /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 064EFB2416715C9F002F0AEC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_ARC = YES; + FRAMEWORK_SEARCH_PATHS = ( + "\"$(SDKROOT)/Developer/Library/Frameworks\"", + "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "SPDYUnitTests/SPDYUnitTests-Prefix.pch"; + INFOPLIST_FILE = "SPDYUnitTests/SPDYUnitTests-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + WRAPPER_EXTENSION = octest; + }; + name = Debug; + }; + 064EFB2516715C9F002F0AEC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_ARC = YES; + FRAMEWORK_SEARCH_PATHS = ( + "\"$(SDKROOT)/Developer/Library/Frameworks\"", + "\"$(DEVELOPER_LIBRARY_DIR)/Frameworks\"", + ); + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "SPDYUnitTests/SPDYUnitTests-Prefix.pch"; + INFOPLIST_FILE = "SPDYUnitTests/SPDYUnitTests-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; + PRODUCT_NAME = "$(TARGET_NAME)"; + VALIDATE_PRODUCT = YES; + WRAPPER_EXTENSION = octest; + }; + name = Release; + }; + 0651EBF116F3F7C800CE44D2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + armv7, + armv7s, + ); + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + DSTROOT = /tmp/SPDY_iphoneos.dst; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "SPDY.iphoneos/SPDY.iphoneos-Prefix.pch"; + IPHONEOS_DEPLOYMENT_TARGET = 5.0; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = iphoneos; + VALID_ARCHS = "armv7 armv7s"; + }; + name = Debug; + }; + 0651EBF216F3F7C800CE44D2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + armv7, + armv7s, + ); + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + DSTROOT = /tmp/SPDY_iphoneos.dst; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "SPDY.iphoneos/SPDY.iphoneos-Prefix.pch"; + IPHONEOS_DEPLOYMENT_TARGET = 5.0; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = iphoneos; + VALIDATE_PRODUCT = YES; + VALID_ARCHS = "armv7 armv7s"; + }; + name = Release; + }; + 0651EC0216F3F7DC00CE44D2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = i386; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + DSTROOT = /tmp/SPDY_iphonesimulator.dst; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "SPDY.iphonesimulator/SPDY.iphonesimulator-Prefix.pch"; + IPHONEOS_DEPLOYMENT_TARGET = 5.0; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphonesimulator; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = iphonesimulator; + VALID_ARCHS = i386; + }; + name = Debug; + }; + 0651EC0316F3F7DC00CE44D2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = i386; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + DSTROOT = /tmp/SPDY_iphonesimulator.dst; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "SPDY.iphonesimulator/SPDY.iphonesimulator-Prefix.pch"; + IPHONEOS_DEPLOYMENT_TARGET = 5.0; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphonesimulator; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = iphonesimulator; + VALIDATE_PRODUCT = YES; + VALID_ARCHS = i386; + }; + name = Release; + }; + 0651EC1216F3F7E500CE44D2 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + COMBINE_HIDPI_IMAGES = YES; + DSTROOT = /tmp/SPDY_macosx.dst; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "SPDY.macosx/SPDY.macosx-Prefix.pch"; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VALID_ARCHS = x86_64; + }; + name = Debug; + }; + 0651EC1316F3F7E500CE44D2 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_64_BIT)"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + COMBINE_HIDPI_IMAGES = YES; + DSTROOT = /tmp/SPDY_macosx.dst; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "SPDY.macosx/SPDY.macosx-Prefix.pch"; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SKIP_INSTALL = YES; + VALIDATE_PRODUCT = YES; + VALID_ARCHS = x86_64; + }; + name = Release; + }; + 0652632516F7B6360081868F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + COMBINE_HIDPI_IMAGES = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SYSTEM_APPS_DIR)/Xcode.app/Contents/Developer/Library/Frameworks\"", + ); + FRAMEWORK_VERSION = A; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "SPDY/SPDY-Prefix.pch"; + INFOPLIST_FILE = "SPDY/SPDY-Info.plist"; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SUPPORTED_PLATFORMS = macosx; + WRAPPER_EXTENSION = framework; + }; + name = Debug; + }; + 0652632616F7B6360081868F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + COMBINE_HIDPI_IMAGES = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SYSTEM_APPS_DIR)/Xcode.app/Contents/Developer/Library/Frameworks\"", + ); + FRAMEWORK_VERSION = A; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "SPDY/SPDY-Prefix.pch"; + INFOPLIST_FILE = "SPDY/SPDY-Info.plist"; + ONLY_ACTIVE_ARCH = NO; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + SUPPORTED_PLATFORMS = macosx; + WRAPPER_EXTENSION = framework; + }; + name = Release; + }; + 06A4E516180F21D500A82D93 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = arm64; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + DSTROOT = /tmp/SPDY_iphoneos.dst; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "SPDY.iphoneos/SPDY.iphoneos-Prefix.pch"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = SPDY.iphoneos.arm64; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = iphoneos; + VALID_ARCHS = arm64; + }; + name = Debug; + }; + 06A4E517180F21D500A82D93 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = arm64; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + DSTROOT = /tmp/SPDY_iphoneos.dst; + GCC_PRECOMPILE_PREFIX_HEADER = YES; + GCC_PREFIX_HEADER = "SPDY.iphoneos/SPDY.iphoneos-Prefix.pch"; + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + ONLY_ACTIVE_ARCH = NO; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = SPDY.iphoneos.arm64; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = iphoneos; + VALIDATE_PRODUCT = YES; + VALID_ARCHS = arm64; + }; + name = Release; + }; + D2CC14BB16179B43002E37CF /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)"; + CONFIGURATION_TEMP_DIR = "$(PROJECT_TEMP_DIR)/$(CONFIGURATION)"; + COPY_PHASE_STRIP = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + 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; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.7; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + D2CC14BC16179B43002E37CF /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ARCHS = "$(ARCHS_STANDARD_32_BIT)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)"; + CONFIGURATION_TEMP_DIR = "$(PROJECT_TEMP_DIR)/$(CONFIGURATION)"; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_ENABLE_OBJC_EXCEPTIONS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.7; + SDKROOT = iphoneos; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 064EFB2316715C9F002F0AEC /* Build configuration list for PBXNativeTarget "SPDYUnitTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 064EFB2416715C9F002F0AEC /* Debug */, + 064EFB2516715C9F002F0AEC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0651EBF316F3F7C800CE44D2 /* Build configuration list for PBXNativeTarget "SPDY.iphoneos" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0651EBF116F3F7C800CE44D2 /* Debug */, + 0651EBF216F3F7C800CE44D2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0651EC0116F3F7DC00CE44D2 /* Build configuration list for PBXNativeTarget "SPDY.iphonesimulator" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0651EC0216F3F7DC00CE44D2 /* Debug */, + 0651EC0316F3F7DC00CE44D2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0651EC1116F3F7E500CE44D2 /* Build configuration list for PBXNativeTarget "SPDY.macosx" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0651EC1216F3F7E500CE44D2 /* Debug */, + 0651EC1316F3F7E500CE44D2 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0652632416F7B6360081868F /* Build configuration list for PBXNativeTarget "SPDY" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0652632516F7B6360081868F /* Debug */, + 0652632616F7B6360081868F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 06A4E515180F21D500A82D93 /* Build configuration list for PBXNativeTarget "SPDY.iphoneos.arm64" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 06A4E516180F21D500A82D93 /* Debug */, + 06A4E517180F21D500A82D93 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + D2CC14A816179B43002E37CF /* Build configuration list for PBXProject "SPDY" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D2CC14BB16179B43002E37CF /* Debug */, + D2CC14BC16179B43002E37CF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = D2CC14A516179B43002E37CF /* Project object */; +} diff --git a/SPDY.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SPDY.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..f7f28ad --- /dev/null +++ b/SPDY.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.iphoneos.arm64.xcscheme b/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.iphoneos.arm64.xcscheme new file mode 100644 index 0000000..a3a4dc5 --- /dev/null +++ b/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.iphoneos.arm64.xcscheme @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.iphoneos.xcscheme b/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.iphoneos.xcscheme new file mode 100644 index 0000000..fbb1249 --- /dev/null +++ b/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.iphoneos.xcscheme @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.iphonesimulator.xcscheme b/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.iphonesimulator.xcscheme new file mode 100644 index 0000000..f98bfca --- /dev/null +++ b/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.iphonesimulator.xcscheme @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.macosx.xcscheme b/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.macosx.xcscheme new file mode 100644 index 0000000..3accc2c --- /dev/null +++ b/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.macosx.xcscheme @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.xcscheme b/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.xcscheme new file mode 100644 index 0000000..f2abf02 --- /dev/null +++ b/SPDY.xcodeproj/xcshareddata/xcschemes/SPDY.xcscheme @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SPDY.xcodeproj/xcshareddata/xcschemes/SPDYUnitTests.xcscheme b/SPDY.xcodeproj/xcshareddata/xcschemes/SPDYUnitTests.xcscheme new file mode 100644 index 0000000..fb980e1 --- /dev/null +++ b/SPDY.xcodeproj/xcshareddata/xcschemes/SPDYUnitTests.xcscheme @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SPDY/NSURLRequest+SPDYURLRequest.h b/SPDY/NSURLRequest+SPDYURLRequest.h new file mode 100644 index 0000000..f222158 --- /dev/null +++ b/SPDY/NSURLRequest+SPDYURLRequest.h @@ -0,0 +1,54 @@ +// +// NSURLRequest+SPDYURLRequest.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import + +@interface NSURLRequest (SPDYURLRequest) + +/** + If present, the stream specified will be used as the HTTP body for the + request. This circumvents a bug in CFNetwork with HTTPBodyStream, but will + not allow a stream to be replayed in the event of an authentication + challenge or redirect. If either of those responses is a possibility, use + HTTPBody or SPDYBodyFile instead. +*/ +@property (nonatomic, readonly) NSInputStream *SPDYBodyStream; + +/** + If present, the file path specified will be used as the HTTP body for the + request. This is the preferred secondary mechanism for specifying the body + of a request when HTTPBody is not sufficient. +*/ +@property (nonatomic, readonly) NSString *SPDYBodyFile; + +/** + Priority per the SPDY draft spec. Defaults to 0. +*/ +@property (nonatomic, readonly) NSUInteger SPDYPriority; + +/** + Specifies whether this request may be deferred until an active session is + available. Defaults to false. +*/ +@property (nonatomic, readonly) BOOL SPDYDiscretionary; + +/** + Request header fields canonicalized to SPDY format. +*/ +- (NSDictionary *)allSPDYHeaderFields; +@end + +@interface NSMutableURLRequest (SPDYURLRequest) +@property (nonatomic) NSInputStream *SPDYBodyStream; +@property (nonatomic) NSString *SPDYBodyFile; +@property (nonatomic) NSUInteger SPDYPriority; +@property (nonatomic) BOOL SPDYDiscretionary; +@end diff --git a/SPDY/NSURLRequest+SPDYURLRequest.m b/SPDY/NSURLRequest+SPDYURLRequest.m new file mode 100644 index 0000000..0162a55 --- /dev/null +++ b/SPDY/NSURLRequest+SPDYURLRequest.m @@ -0,0 +1,159 @@ +// +// NSURLRequest+SPDYURLRequest.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "NSURLRequest+SPDYURLRequest.h" +#import "SPDYProtocol.h" + +@implementation NSURLRequest (SPDYURLRequest) + +- (NSUInteger)SPDYPriority +{ + return [[SPDYProtocol propertyForKey:@"SPDYPriority" inRequest:self] unsignedIntegerValue]; +} + +- (BOOL)SPDYDiscretionary +{ + return [[SPDYProtocol propertyForKey:@"SPDYDiscretionary" inRequest:self] boolValue]; +} + +- (NSInputStream *)SPDYBodyStream +{ + return [SPDYProtocol propertyForKey:@"SPDYBodyStream" inRequest:self]; +} + +- (NSString *)SPDYBodyFile +{ + return [SPDYProtocol propertyForKey:@"SPDYBodyFile" inRequest:self]; +} + +- (NSDictionary *)allSPDYHeaderFields +{ + NSDictionary *httpHeaders = self.allHTTPHeaderFields; + + static NSSet *invalidKeys; + static NSSet *reservedKeys; + static dispatch_once_t initialized; + dispatch_once(&initialized, ^{ + invalidKeys = [[NSSet alloc] initWithObjects: + @"connection", @"keep-alive", @"proxy-connection", @"transfer-encoding", nil + ]; + + reservedKeys = [[NSSet alloc] initWithObjects: + @"method", @"path", @"version", @"host", @"scheme", nil + ]; + }); + + NSMutableString *path = [[NSMutableString alloc] initWithString:self.URL.path]; + NSString *query = self.URL.query; + if (query) { + [path appendFormat:@"?%@", query]; + } + + NSString *fragment = self.URL.fragment; + if (fragment) { + [path appendFormat:@"#%@", fragment]; + } + + // Allow manually-set headers to override request properties + NSMutableDictionary *spdyHeaders = [[NSMutableDictionary alloc] initWithDictionary:@{ + @":method" : self.HTTPMethod, + @":path" : path, + @":version" : @"HTTP/1.1", + @":host" : self.URL.host, + @":scheme" : self.URL.scheme + }]; + + bool hasBodyData = (self.HTTPBody || self.HTTPBodyStream + || self.SPDYBodyStream || self.SPDYBodyFile); + if ([self.HTTPMethod isEqualToString:@"POST"] && hasBodyData) { + spdyHeaders[@"content-type"] = @"application/x-www-form-urlencoded"; + } + + for (NSString *key in httpHeaders) { + NSString *lowercaseKey = [key lowercaseString]; + if (![invalidKeys containsObject:lowercaseKey]) { + if ([reservedKeys containsObject:lowercaseKey]) { + spdyHeaders[[@":" stringByAppendingString:lowercaseKey]] = httpHeaders[key]; + } else { + spdyHeaders[lowercaseKey] = httpHeaders[key]; + } + } + } + + if (self.HTTPShouldHandleCookies) { + NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:self.URL]; + if (cookies.count > 0) { + NSDictionary *cookieHeaders = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies]; + spdyHeaders[@"cookie"] = cookieHeaders[@"Cookie"]; + } + } + + // Request properties will take precedence over manually-set headers + // [spdyHeaders addEntriesFromDictionary:@{ + // @":method" : self.HTTPMethod, + // @":path" : path, + // @":version" : @"HTTP/1.1", + // @":host" : self.URL.host, + // @":scheme" : self.URL.scheme + // }]; + + return spdyHeaders; +} + +@end + +@implementation NSMutableURLRequest (SPDYURLRequest) + +- (NSUInteger)SPDYPriority +{ + return [[SPDYProtocol propertyForKey:@"SPDYPriority" inRequest:self] unsignedIntegerValue]; +} + +- (void)setSPDYPriority:(NSUInteger)priority +{ + [SPDYProtocol setProperty:@(priority) forKey:@"SPDYPriority" inRequest:self]; +} + +- (BOOL)SPDYDiscretionary +{ + return [[SPDYProtocol propertyForKey:@"SPDYDiscretionary" inRequest:self] boolValue]; +} + +- (void)setSPDYDiscretionary:(BOOL)Discretionary +{ + [SPDYProtocol setProperty:@(Discretionary) forKey:@"SPDYDiscretionary" inRequest:self]; +} + +- (NSInputStream *)SPDYBodyStream +{ + return [SPDYProtocol propertyForKey:@"SPDYBodyStream" inRequest:self]; +} + +- (void)setSPDYBodyStream:(NSInputStream *)SPDYBodyStream +{ + [SPDYProtocol setProperty:SPDYBodyStream forKey:@"SPDYBodyStream" inRequest:self]; +} + +- (NSString *)SPDYBodyFile +{ + return [SPDYProtocol propertyForKey:@"SPDYBodyFile" inRequest:self]; +} + +- (void)setSPDYBodyFile:(NSString *)SPDYBodyFile +{ + [SPDYProtocol setProperty:SPDYBodyFile forKey:@"SPDYBodyFile" inRequest:self]; +} + +@end diff --git a/SPDY/SPDY-Info.plist b/SPDY/SPDY-Info.plist new file mode 100644 index 0000000..f03b925 --- /dev/null +++ b/SPDY/SPDY-Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + + CFBundleIdentifier + com.twitter.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSHumanReadableCopyright + Copyright © 2013 Twitter. All rights reserved. + NSPrincipalClass + + + diff --git a/SPDY/SPDY-Prefix.pch b/SPDY/SPDY-Prefix.pch new file mode 100644 index 0000000..bc13e0a --- /dev/null +++ b/SPDY/SPDY-Prefix.pch @@ -0,0 +1,8 @@ +// +// Prefix header for all source files of the 'SPDY' target in the 'SPDY' project +// + +#ifdef __OBJC__ + #import +#endif + diff --git a/SPDY/SPDYCommonLogger.h b/SPDY/SPDYCommonLogger.h new file mode 100644 index 0000000..bb71b57 --- /dev/null +++ b/SPDY/SPDYCommonLogger.h @@ -0,0 +1,49 @@ +// +// SPDYCommonLogger.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import +#import "SPDYLogger.h" + +#ifdef DEBUG +#define SPDY_LOG_LEVEL 4 +#else +#define SPDY_LOG_LEVEL 0 +#endif + +#define SPDY_DEBUG(message, ...) +#define SPDY_INFO(message, ...) +#define SPDY_WARNING(message, ...) +#define SPDY_ERROR(message, ...) + +#if SPDY_LOG_LEVEL >= 3 +#undef SPDY_DEBUG +#define SPDY_DEBUG(message, ...) [SPDYCommonLogger log:message atLevel:SPDYLogLevelDebug, ##__VA_ARGS__] +#endif + +#if SPDY_LOG_LEVEL >= 2 +#undef SPDY_INFO +#define SPDY_INFO(message, ...) [SPDYCommonLogger log:message atLevel:SPDYLogLevelInfo, ##__VA_ARGS__] +#endif + +#if SPDY_LOG_LEVEL >= 1 +#undef SPDY_WARNING +#define SPDY_WARNING(message, ...) [SPDYCommonLogger log:message atLevel:SPDYLogLevelWarning, ##__VA_ARGS__] +#endif + +#if SPDY_LOG_LEVEL >= 0 +#undef SPDY_ERROR +#define SPDY_ERROR(message, ...) [SPDYCommonLogger log:message atLevel:SPDYLogLevelError, ##__VA_ARGS__] +#endif + +@interface SPDYCommonLogger : NSObject ++ (void)setLogger:(id)logger; ++ (void)log:(NSString *)format atLevel:(SPDYLogLevel)level, ... NS_FORMAT_FUNCTION(1,3); +@end diff --git a/SPDY/SPDYCommonLogger.m b/SPDY/SPDYCommonLogger.m new file mode 100644 index 0000000..6fadba7 --- /dev/null +++ b/SPDY/SPDYCommonLogger.m @@ -0,0 +1,62 @@ +// +// SPDYCommonLogger.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "SPDYCommonLogger.h" + +#ifdef DEBUG +#define SPDY_DEBUG_LOGGING 1 +#endif + +static const NSString *logLevels[4] = { @"ERROR", @"WARNING", @"INFO", @"DEBUG" }; + +@implementation SPDYCommonLogger + +static dispatch_once_t initialized; +static dispatch_queue_t loggerQueue; +static id sharedLogger; + ++ (void)initialize +{ + dispatch_once(&initialized, ^{ + loggerQueue = dispatch_queue_create("com.twitter.SPDYProtocolLoggerQueue", DISPATCH_QUEUE_SERIAL); + }); +} + ++ (void)setLogger:(id)logger +{ + dispatch_async(loggerQueue, ^{ + sharedLogger = logger; + }); +} + ++ (void)log:(NSString *)format atLevel:(SPDYLogLevel)level, ... NS_FORMAT_FUNCTION(1,3) +{ + va_list args; + va_start(args, level); + NSString *message = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + +#if SPDY_DEBUG_LOGGING + NSLog(@"SPDY [%@] %@", logLevels[level], message); +#else + dispatch_async(loggerQueue, ^{ + if (sharedLogger) { + [sharedLogger log:message atLevel:level]; + } + }); +#endif +} + +@end diff --git a/SPDY/SPDYDefinitions.h b/SPDY/SPDYDefinitions.h new file mode 100644 index 0000000..6e4d752 --- /dev/null +++ b/SPDY/SPDYDefinitions.h @@ -0,0 +1,79 @@ +// +// SPDYDefinitions.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +typedef enum : uint32_t { + SPDY_SYN_STREAM_FRAME = 1, + SPDY_SYN_REPLY_FRAME, + SPDY_RST_STREAM_FRAME, + SPDY_SETTINGS_FRAME, + SPDY_NOOP_FRAME, + SPDY_PING_FRAME, + SPDY_GOAWAY_FRAME, + SPDY_HEADERS_FRAME, + SPDY_WINDOW_UPDATE_FRAME, + SPDY_CREDENTIAL_FRAME +} SPDYControlFrameType; + +typedef enum : uint32_t { + SPDY_STREAM_PROTOCOL_ERROR = 1, + SPDY_STREAM_INVALID_STREAM, + SPDY_STREAM_REFUSED_STREAM, + SPDY_STREAM_UNSUPPORTED_VERSION, + SPDY_STREAM_CANCEL, + SPDY_STREAM_INTERNAL_ERROR, + SPDY_STREAM_FLOW_CONTROL_ERROR, + SPDY_STREAM_STREAM_IN_USE, + SPDY_STREAM_STREAM_ALREADY_CLOSED, + SPDY_STREAM_INVALID_CREDENTIALS, + SPDY_STREAM_FRAME_TOO_LARGE +} SPDYStreamStatus; + +typedef enum : uint32_t { + SPDY_SESSION_OK = 0, + SPDY_SESSION_PROTOCOL_ERROR, + SPDY_SESSION_INTERNAL_ERROR +} SPDYSessionStatus; + +typedef enum : uint32_t { + _SPDY_SETTINGS_RANGE_START = 0, + SPDY_SETTINGS_MINOR_VERSION = _SPDY_SETTINGS_RANGE_START, + SPDY_SETTINGS_UPLOAD_BANDWIDTH, + SPDY_SETTINGS_DOWNLOAD_BANDWIDTH, + SPDY_SETTINGS_ROUND_TRIP_TIME, + SPDY_SETTINGS_MAX_CONCURRENT_STREAMS, + SPDY_SETTINGS_CURRENT_CWND, + SPDY_SETTINGS_DOWNLOAD_RETRANS_RATE, + SPDY_SETTINGS_INITIAL_WINDOW_SIZE, + SPDY_SETTINGS_CLIENT_CERTIFICATE_VECTOR_SIZE, + _SPDY_SETTINGS_RANGE_END, +} SPDYSettingsId; + +#define SPDY_SETTINGS_LENGTH _SPDY_SETTINGS_RANGE_END +#define SPDY_SETTINGS_ITERATOR(i) for (SPDYSettingsId i = _SPDY_SETTINGS_RANGE_START; i < _SPDY_SETTINGS_RANGE_END; i++) + +typedef struct { + bool set; + uint8_t flags; + int32_t value; +} SPDYSettings; + +typedef uint32_t SPDYStreamId; + +static const SPDYStreamId kSPDYSessionStreamId = 0; + +static const uint8_t SPDY_FLAG_FIN = 0x01; +static const uint8_t SPDY_FLAG_UNIDIRECTIONAL = 0x02; + +static const uint8_t SPDY_DATA_FLAG_FIN = 0x01; + +static const uint8_t SPDY_SETTINGS_FLAG_CLEAR_SETTINGS = 0x01; +static const uint8_t SPDY_SETTINGS_FLAG_PERSIST_VALUE = 0x01; +static const uint8_t SPDY_SETTINGS_FLAG_PERSISTED = 0x02; diff --git a/SPDY/SPDYError.h b/SPDY/SPDYError.h new file mode 100644 index 0000000..51dbe0d --- /dev/null +++ b/SPDY/SPDYError.h @@ -0,0 +1,61 @@ +// +// SPDYError.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +extern NSString *const SPDYStreamErrorDomain; +extern NSString *const SPDYSessionErrorDomain; +extern NSString *const SPDYCodecErrorDomain; +extern NSString *const SPDYSocketErrorDomain; + +typedef enum { + SPDYStreamProtocolError = 1, + SPDYStreamInvalidStream, + SPDYStreamRefusedStream, + SPDYStreamUnsupportedVersion, + SPDYStreamCancel, + SPDYStreamInternalError, + SPDYStreamFlowControlError, + SPDYStreamStreamInUse, + SPDYStreamStreamAlreadyClosed, + SPDYStreamInvalidCredentials, + SPDYStreamFrameTooLarge +} SPDYStreamError; + +typedef enum { + SPDYSessionProtocolError = 1, + SPDYSessionInternalError +} SPDYSessionError; + +typedef enum { + SDPYHeaderBlockEncodingError = 1, + SDPYHeaderBlockDecodingError +} SPDYCodecError; + +typedef enum { + SPDYSocketCFSocketError = kCFSocketError, // From CFSocketError enum. + SPDYSocketConnectCanceled = 1, // socketWillConnect: returned NO. + SPDYSocketConnectTimeout, + SPDYSocketReadTimeout, + SPDYSocketWriteTimeout, + SPDYSocketTLSVerificationFailed, + SPDYSocketTransportError +} SPDYSocketError; + +#define SPDY_STREAM_ERROR(CODE, MESSAGE) \ + [[NSError alloc] initWithDomain:SPDYStreamErrorDomain code:CODE userInfo:@{ NSLocalizedDescriptionKey: MESSAGE}] + +#define SPDY_SESSION_ERROR(CODE, MESSAGE) \ + [[NSError alloc] initWithDomain:SPDYSessionErrorDomain code:CODE userInfo:@{ NSLocalizedDescriptionKey: MESSAGE}] + +#define SPDY_SOCKET_ERROR(CODE, MESSAGE) \ + [[NSError alloc] initWithDomain:SPDYSocketErrorDomain code:CODE userInfo:@{ NSLocalizedDescriptionKey: MESSAGE}] + +#define SPDY_CODEC_ERROR(CODE, MESSAGE) \ + [[NSError alloc] initWithDomain:SPDYCodecErrorDomain code:CODE userInfo:@{ NSLocalizedDescriptionKey: MESSAGE}] diff --git a/SPDY/SPDYFrame.h b/SPDY/SPDYFrame.h new file mode 100644 index 0000000..613fb75 --- /dev/null +++ b/SPDY/SPDYFrame.h @@ -0,0 +1,69 @@ +// +// SPDYFrame.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import +#import "SPDYDefinitions.h" + +@interface SPDYFrame : NSObject +@end + +@interface SPDYHeaderBlockFrame : SPDYFrame +@property (nonatomic, strong) NSDictionary *headers; +@end + +@interface SPDYDataFrame : SPDYFrame +@property (nonatomic, strong) NSData *data; +@property (nonatomic) SPDYStreamId streamId; +@property (nonatomic) bool last; +@end + +@interface SPDYSynStreamFrame : SPDYHeaderBlockFrame +@property (nonatomic) SPDYStreamId streamId; +@property (nonatomic) SPDYStreamId associatedToStreamId; +@property (nonatomic) uint8_t priority; +@property (nonatomic) uint8_t slot; +@property (nonatomic) bool last; +@property (nonatomic) bool unidirectional; +@end + +@interface SPDYSynReplyFrame : SPDYHeaderBlockFrame +@property (nonatomic) SPDYStreamId streamId; +@property (nonatomic) bool last; +@end + +@interface SPDYRstStreamFrame : SPDYFrame +@property (nonatomic) SPDYStreamId streamId; +@property (nonatomic) SPDYStreamStatus statusCode; +@end + +@interface SPDYSettingsFrame : SPDYFrame +@property (nonatomic, readonly) SPDYSettings *settings; +@property (nonatomic) bool clearSettings; +@end + +@interface SPDYPingFrame : SPDYFrame +@property (nonatomic) uint32_t id; +@end + +@interface SPDYGoAwayFrame : SPDYFrame +@property (nonatomic) SPDYStreamId lastGoodStreamId; +@property (nonatomic) SPDYSessionStatus statusCode; +@end + +@interface SPDYHeadersFrame : SPDYHeaderBlockFrame +@property (nonatomic) SPDYStreamId streamId; +@property (nonatomic) bool last; +@end + +@interface SPDYWindowUpdateFrame : SPDYFrame +@property (nonatomic) SPDYStreamId streamId; +@property (nonatomic) uint32_t deltaWindowSize; +@end diff --git a/SPDY/SPDYFrame.m b/SPDY/SPDYFrame.m new file mode 100644 index 0000000..83c87e5 --- /dev/null +++ b/SPDY/SPDYFrame.m @@ -0,0 +1,69 @@ +// +// SPDYFrame.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "SPDYFrame.h" + +@implementation SPDYFrame +@end + +@implementation SPDYHeaderBlockFrame +@end + +@implementation SPDYDataFrame +@end + +@implementation SPDYSynStreamFrame +@end + +@implementation SPDYSynReplyFrame +@end + +@implementation SPDYRstStreamFrame +@end + +@implementation SPDYSettingsFrame +{ + SPDYSettings _settings[SPDY_SETTINGS_LENGTH]; +} + +- (id)init +{ + self = [super init]; + if (self) { + SPDY_SETTINGS_ITERATOR(i) { + _settings[i].set = NO; + } + } + return self; +} + +- (SPDYSettings *)settings +{ + return _settings; +} + +@end + +@implementation SPDYPingFrame +@end + +@implementation SPDYGoAwayFrame +@end + +@implementation SPDYHeadersFrame +@end + +@implementation SPDYWindowUpdateFrame +@end diff --git a/SPDY/SPDYFrameDecoder.h b/SPDY/SPDYFrameDecoder.h new file mode 100644 index 0000000..bbb5ec5 --- /dev/null +++ b/SPDY/SPDYFrameDecoder.h @@ -0,0 +1,39 @@ +// +// SPDYFrameDecoder.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import +#import "SPDYFrame.h" + +@class SPDYFrameDecoder; + +@protocol SPDYFrameDecoderDelegate + +- (void)didReadDataFrame:(SPDYDataFrame *)dataFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder; +- (void)didReadSynStreamFrame:(SPDYSynStreamFrame *)synStreamFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder; +- (void)didReadSynReplyFrame:(SPDYSynReplyFrame *)synReplyFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder; +- (void)didReadRstStreamFrame:(SPDYRstStreamFrame *)rstStreamFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder; +- (void)didReadSettingsFrame:(SPDYSettingsFrame *)settingsFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder; +- (void)didReadPingFrame:(SPDYPingFrame *)pingFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder; +- (void)didReadGoAwayFrame:(SPDYGoAwayFrame *)goAwayFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder; +- (void)didReadHeadersFrame:(SPDYHeadersFrame *)headersFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder; +- (void)didReadWindowUpdateFrame:(SPDYWindowUpdateFrame *)windowUpdateFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder; + +@end + +@interface SPDYFrameDecoder : NSObject +@property (nonatomic, weak) id delegate; + +- (id)initWithDelegate:(id)delegate; + +// returns the number of bytes consumed; the caller is responsible for accumulating unconsumed bytes +- (NSUInteger)decode:(uint8_t *)buffer length:(NSUInteger)len error:(NSError **)pError; + +@end diff --git a/SPDY/SPDYFrameDecoder.m b/SPDY/SPDYFrameDecoder.m new file mode 100644 index 0000000..f0fbef1 --- /dev/null +++ b/SPDY/SPDYFrameDecoder.m @@ -0,0 +1,523 @@ +// +// SPDYFrameDecoder.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "SPDYFrameDecoder.h" +#import "SPDYHeaderBlockDecompressor.h" + +#define SPDY_VERSION 3 +#define SPDY_COMMON_HEADER_SIZE 8 +#define MAX_HEADER_BLOCK_LENGTH 32768 + +typedef struct { + bool ctrl; + union { + struct { + uint16_t version; + uint16_t type; + } control; + struct { + uint32_t streamId; + } data; + }; + uint8_t flags; + uint32_t length; +} SPDYCommonHeader; + +typedef enum { + READ_COMMON_HEADER, + READ_CONTROL_FRAME, + READ_SETTINGS, + READ_HEADER_BLOCK, + READ_DATA_FRAME, + FRAME_ERROR +} SPDYFrameDecoderState; + +@interface SPDYFrameDecoder () +- (NSUInteger)_readCommonHeader:(uint8_t *)buffer length:(NSUInteger)len; +- (NSUInteger)_readControlFrame:(uint8_t *)buffer length:(NSUInteger)len; +- (NSUInteger)_readDataFrame:(uint8_t *)buffer length:(NSUInteger)len; +- (NSUInteger)_readHeaderBlock:(uint8_t *)buffer length:(NSUInteger)len; +- (NSUInteger)_readSettings:(uint8_t *)buffer length:(NSUInteger)len; +@end + +@implementation SPDYFrameDecoder +{ + SPDYHeaderBlockDecompressor *_decompressor; + SPDYHeaderBlockFrame *_headerBlockFrame; + SPDYSettingsFrame *_settingsFrame; + SPDYCommonHeader _header; + SPDYControlFrameType _type; + SPDYFrameDecoderState _state; + NSUInteger _decompressedLength; + NSUInteger _length; + NSUInteger _maxHeaderBlockLength; + uint8_t *_decompressed; +} + +int32_t getSignedInt32(uint8_t *buffer) { + return ntohl(*(int32_t *)buffer); +} + +uint32_t getUnsignedInt32(uint8_t *buffer) { + return ntohl(*(uint32_t *)buffer); +} + +uint32_t getUnsignedInt31(uint8_t *buffer) { + return getUnsignedInt32(buffer) & 0x7FFFFFFF; +} + +uint32_t getUnsignedInt24(uint8_t *buffer) { + return getUnsignedInt32(buffer) & 0x00FFFFFF; +} + +uint16_t getUnsignedInt16(uint8_t *buffer) { + return ntohs(*(uint16_t *)buffer); +} + +uint16_t getUnsignedInt15(uint8_t *buffer) { + return getUnsignedInt16(buffer) & 0x7FFF; +} + +SPDYCommonHeader getCommonHeader(uint8_t *buffer) { + SPDYCommonHeader header; + header.ctrl = (buffer[0] & 0x80) != 0; + if (header.ctrl) { + header.control.version = getUnsignedInt15(buffer); + header.control.type = getUnsignedInt16(buffer + 2); + } else { + header.data.streamId = getUnsignedInt32(buffer); + } + header.flags = buffer[4]; + header.length = getUnsignedInt24(buffer + 4); + return header; +} + + +- (id)initWithDelegate:(id)delegate +{ + self = [super init]; + if (self) { + _delegate = delegate; + _maxHeaderBlockLength = MAX_HEADER_BLOCK_LENGTH; + _state = READ_COMMON_HEADER; + + _decompressor = [[SPDYHeaderBlockDecompressor alloc] init]; + _decompressed = malloc(sizeof(uint8_t) * MAX_HEADER_BLOCK_LENGTH); + _decompressedLength = 0; + } + return self; +} + +- (void)dealloc +{ + free(_decompressed); +} + +- (NSUInteger)decode:(uint8_t *)buffer length:(NSUInteger)len error:(NSError **)pError; +{ + NSUInteger bytesRead = 0; + NSUInteger totalBytesRead = 0; + + SPDYFrameDecoderState previousState; + do { + previousState = _state; + + switch (_state) { + case READ_COMMON_HEADER: + bytesRead = [self _readCommonHeader:buffer length:len]; + break; + case READ_CONTROL_FRAME: + bytesRead = [self _readControlFrame:buffer length:len]; + break; + case READ_SETTINGS: + bytesRead = [self _readSettings:buffer length:len]; + break; + case READ_HEADER_BLOCK: + bytesRead = [self _readHeaderBlock:buffer length:len]; + break; + case READ_DATA_FRAME: + bytesRead = [self _readDataFrame:buffer length:len]; + break; + case FRAME_ERROR: + bytesRead = 0; + if (pError) { + *pError = [[NSError alloc] initWithDomain:@"com.twitter.spdy.decoder" + code:SPDY_SESSION_PROTOCOL_ERROR + userInfo:nil]; + } + break; + } + + totalBytesRead += bytesRead; + buffer += bytesRead; + len -= bytesRead; + } while (previousState != _state); + + return totalBytesRead; +} + +#pragma mark private methods + +- (NSUInteger)_readCommonHeader:(uint8_t *)buffer length:(NSUInteger)len +{ + if (SPDY_COMMON_HEADER_SIZE <= len) { + _header = getCommonHeader(buffer); + if (_header.ctrl) { + if (_header.control.version == SPDY_VERSION) { + _type = (SPDYControlFrameType) _header.control.type; + _length = _header.length; + _state = READ_CONTROL_FRAME; + } else { + _state = FRAME_ERROR; + } + } else { + _length = _header.length; + _state = READ_DATA_FRAME; + } + + return SPDY_COMMON_HEADER_SIZE; + } else { + return 0; + } +} + +- (NSUInteger)_readControlFrame:(uint8_t *)buffer length:(NSUInteger)len +{ + NSUInteger bytesRead = 0; + NSUInteger minLength; + + switch (_type) { + + /* Header block frames */ + + case SPDY_SYN_STREAM_FRAME: + minLength = 10; + if (minLength <= len) { + SPDYSynStreamFrame *frame = [[SPDYSynStreamFrame alloc] init]; + + frame.last = _header.flags & SPDY_FLAG_FIN; + frame.unidirectional = _header.flags & SPDY_FLAG_UNIDIRECTIONAL; + + frame.streamId = getUnsignedInt31(buffer); + frame.associatedToStreamId = getUnsignedInt31(buffer+4); + frame.priority = buffer[8] >> 5 & 0x07; + + bytesRead = minLength; + + _headerBlockFrame = frame; + _state = READ_HEADER_BLOCK; + } + break; + + case SPDY_SYN_REPLY_FRAME: + minLength = 4; + if (minLength <= len) { + SPDYSynReplyFrame *frame = [[SPDYSynReplyFrame alloc] init]; + + frame.last = _header.flags & SPDY_FLAG_FIN; + frame.streamId = getUnsignedInt31(buffer); + bytesRead = minLength; + + _headerBlockFrame = frame; + _state = READ_HEADER_BLOCK; + } + break; + + case SPDY_HEADERS_FRAME: + minLength = 4; + if (minLength <= len) { + SPDYHeadersFrame *frame = [[SPDYHeadersFrame alloc] init]; + + frame.last = _header.flags & SPDY_FLAG_FIN; + frame.streamId = getUnsignedInt31(buffer); + bytesRead = minLength; + + _headerBlockFrame = frame; + _state = READ_HEADER_BLOCK; + } + break; + + /* Settings frame */ + + case SPDY_SETTINGS_FRAME: + minLength = 4; + if (minLength <= len) { + SPDYSettingsFrame *frame = [[SPDYSettingsFrame alloc] init]; + frame.clearSettings = _header.flags & SPDY_SETTINGS_FLAG_CLEAR_SETTINGS; + + NSUInteger settingsCount = getUnsignedInt32(buffer); + bytesRead = minLength; + + NSUInteger lengthRemaining = _length - bytesRead; + + // "gangnam-style" -JP + if ((lengthRemaining & 0x07) != 0 || lengthRemaining >> 3 != settingsCount) { + _state = FRAME_ERROR; + return bytesRead; + } + + _settingsFrame = frame; + _state = READ_SETTINGS; + } + + break; + + /* Fixed-length frames */ + + case SPDY_RST_STREAM_FRAME: + minLength = 8; + if (minLength <= len) { + SPDYRstStreamFrame *frame = [[SPDYRstStreamFrame alloc] init]; + frame.streamId = getUnsignedInt31(buffer); + frame.statusCode = (SPDYStreamStatus)getUnsignedInt32(buffer+4); + bytesRead = minLength; + [_delegate didReadRstStreamFrame:frame frameDecoder:self]; + _state = READ_COMMON_HEADER; + } + break; + + case SPDY_PING_FRAME: + minLength = 4; + if (minLength <= len) { + SPDYPingFrame *frame = [[SPDYPingFrame alloc] init]; + frame.id = getUnsignedInt32(buffer); + bytesRead = minLength; + [_delegate didReadPingFrame:frame frameDecoder:self]; + _state = READ_COMMON_HEADER; + } + break; + + case SPDY_GOAWAY_FRAME: + minLength = 8; + if (minLength <= len) { + SPDYGoAwayFrame *frame = [[SPDYGoAwayFrame alloc] init]; + frame.lastGoodStreamId = getUnsignedInt31(buffer); + frame.statusCode = (SPDYSessionStatus)getUnsignedInt32(buffer+4); + bytesRead = minLength; + [_delegate didReadGoAwayFrame:frame frameDecoder:self]; + _state = READ_COMMON_HEADER; + } + break; + + case SPDY_WINDOW_UPDATE_FRAME: + minLength = 8; + if (minLength <= len) { + SPDYWindowUpdateFrame *frame = [[SPDYWindowUpdateFrame alloc] init]; + frame.streamId = getUnsignedInt31(buffer); + frame.deltaWindowSize = getUnsignedInt31(buffer+4); + bytesRead = minLength; + [_delegate didReadWindowUpdateFrame:frame frameDecoder:self]; + _state = READ_COMMON_HEADER; + + } + break; + + default: + _state = FRAME_ERROR; + } + + _length -= bytesRead; + return bytesRead; +} + +- (NSUInteger)_readDataFrame:(uint8_t *)buffer length:(NSUInteger)len +{ + NSUInteger streamId = _header.data.streamId; + if (streamId == 0) { + _state = FRAME_ERROR; + return 0; + } + + NSUInteger bytesToRead = MIN(len, _length); + NSUInteger bytesRead = 0; + + // Skip production of non-terminating 0-length frames, in other words, + // don't produce anything for now if we can't make progress and either + // - this frame read is still incomplete + // - or this is not the last frame on the stream + if (bytesToRead == 0 && (_length > 0 || ((_header.flags & SPDY_DATA_FLAG_FIN) == 0))) { + return 0; + } + + SPDYDataFrame *frame = [[SPDYDataFrame alloc] init]; + frame.streamId = streamId; + frame.data = [[NSData alloc] initWithBytesNoCopy:buffer + length:bytesToRead + freeWhenDone:NO]; + + bytesRead = bytesToRead; + _length -= bytesToRead; + + if (_length == 0) { + frame.last = ((_header.flags & SPDY_DATA_FLAG_FIN) != 0); + _state = READ_COMMON_HEADER; + } + + // The delegate is only guaranteed access to the NSData object on the frame + // for the duration of this call. It has no ownership of the buffer and must + // perform all processing or copy the data elsewhere prior to returning. + [_delegate didReadDataFrame:frame frameDecoder:self]; + + return bytesRead; +} + +- (NSUInteger)_readHeaderBlock:(uint8_t *)buffer length:(NSUInteger)len +{ + NSUInteger bytesToRead = MIN(_length, len); + NSUInteger bytesRead = 0; + NSError *error = nil; + + _decompressedLength += [_decompressor inflate:buffer + availIn:bytesToRead + outputBuffer:(_decompressed + _decompressedLength) + availOut:(_maxHeaderBlockLength - _decompressedLength) + error:&error]; + + + bytesRead = bytesToRead; + + if (error) { + _state = FRAME_ERROR; + return bytesRead; + } + + _length -= bytesRead; + + if (_length == 0) { + if (_headerBlockFrame != nil) { + NSMutableDictionary *headers = [[NSMutableDictionary alloc] init]; + NSUInteger headerNameLength, headerValueLength; + + if (_decompressedLength < 4) { + _state = FRAME_ERROR; + return bytesRead; + } + + NSUInteger headerCount = getUnsignedInt32(_decompressed); + NSUInteger bufferIndex = 4; + + while (headerCount > 0 && bufferIndex < _decompressedLength) { + + /* Read header name length */ + + if (bufferIndex + 4 > _decompressedLength) { + _state = FRAME_ERROR; + return bytesRead; + } + + headerNameLength = getUnsignedInt32(_decompressed + bufferIndex); + bufferIndex += 4; + + /* Read header name */ + + if (bufferIndex + headerNameLength > _decompressedLength) { + _state = FRAME_ERROR; + return bytesRead; + } + + NSString *headerName = [[NSString alloc] initWithBytes:(_decompressed + bufferIndex) length:headerNameLength encoding:NSUTF8StringEncoding]; + bufferIndex += headerNameLength; + + /* Read header value length */ + + if (bufferIndex + 4 > _decompressedLength) { + _state = FRAME_ERROR; + return bytesRead; + } + + headerValueLength = getUnsignedInt32(_decompressed + bufferIndex); + bufferIndex += 4; + + /* Read header value */ + + if (bufferIndex + headerValueLength > _decompressedLength) { + _state = FRAME_ERROR; + return bytesRead; + } + + NSString *headerValue = [[NSString alloc] initWithBytes:(_decompressed + bufferIndex) length:headerValueLength encoding:NSUTF8StringEncoding]; + bool arrayValue = NO; + for (int i = 1; !arrayValue && i < headerValueLength - 1; i++) { + if (_decompressed[bufferIndex + i] == '\0') { + arrayValue = YES; + } + } + bufferIndex += headerValueLength; + + if (arrayValue) { + headers[headerName] = [headerValue componentsSeparatedByString:@"\0"]; + } else { + headers[headerName] = headerValue; + } + + headerCount--; + } + + _headerBlockFrame.headers = headers; + + switch (_type) { + case SPDY_SYN_STREAM_FRAME: + [_delegate didReadSynStreamFrame:(SPDYSynStreamFrame *) _headerBlockFrame frameDecoder:self]; + break; + case SPDY_SYN_REPLY_FRAME: + [_delegate didReadSynReplyFrame:(SPDYSynReplyFrame *) _headerBlockFrame frameDecoder:self]; + break; + case SPDY_HEADERS_FRAME: + [_delegate didReadHeadersFrame:(SPDYHeadersFrame *) _headerBlockFrame frameDecoder:self]; + break; + default: + // Should never happen + break; + } + } + + _decompressedLength = 0; + _state = READ_COMMON_HEADER; + } + + return bytesRead; +} + +- (NSUInteger)_readSettings:(uint8_t *)buffer length:(NSUInteger)len +{ + NSUInteger bytesToRead = MIN(_length, len); + NSUInteger bytesRead = 0; + + while (bytesRead + 8 <= bytesToRead) { + uint8_t flags = buffer[bytesRead]; + SPDYSettingsId settingsId = (SPDYSettingsId)getUnsignedInt24(buffer + bytesRead); + bytesRead += 4; + + int32_t value = getSignedInt32(buffer + bytesRead); + bytesRead += 4; + + if (settingsId >= _SPDY_SETTINGS_RANGE_START && settingsId < _SPDY_SETTINGS_RANGE_END && !_settingsFrame.settings[settingsId].set) { + _settingsFrame.settings[settingsId].set = YES; + _settingsFrame.settings[settingsId].flags = flags; + _settingsFrame.settings[settingsId].value = value; + } + } + + _length -= bytesRead; + + if (_length == 0) { + [_delegate didReadSettingsFrame:_settingsFrame frameDecoder:self]; + _state = READ_COMMON_HEADER; + } + + return bytesRead; +} + +@end diff --git a/SPDY/SPDYFrameEncoder.h b/SPDY/SPDYFrameEncoder.h new file mode 100644 index 0000000..91b7fbe --- /dev/null +++ b/SPDY/SPDYFrameEncoder.h @@ -0,0 +1,33 @@ +// +// SPDYFrameEncoder.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import +#import "SPDYFrame.h" + +@class SPDYFrameEncoder; + +@protocol SPDYFrameEncoderDelegate +- (void)didEncodeData:(NSData *)data frameEncoder:(SPDYFrameEncoder *)encoder; +@end + +@interface SPDYFrameEncoder : NSObject +@property (nonatomic, weak) id delegate; +- (id)initWithDelegate:(id )delegate headerCompressionLevel:(NSUInteger)headerCompressionLevel; +- (bool)encodeDataFrame:(SPDYDataFrame *)dataFrame; +- (bool)encodeSynStreamFrame:(SPDYSynStreamFrame *)synStreamFrame; +- (bool)encodeSynReplyFrame:(SPDYSynReplyFrame *)synReplyFrame; +- (bool)encodeRstStreamFrame:(SPDYRstStreamFrame *)rstStreamFrame; +- (bool)encodeSettingsFrame:(SPDYSettingsFrame *)settingsFrame; +- (bool)encodePingFrame:(SPDYPingFrame *)pingFrame; +- (bool)encodeGoAwayFrame:(SPDYGoAwayFrame *)goAwayFrame; +- (bool)encodeHeadersFrame:(SPDYHeadersFrame *)headersFrame; +- (bool)encodeWindowUpdateFrame:(SPDYWindowUpdateFrame *)windowUpdateFrame; +@end diff --git a/SPDY/SPDYFrameEncoder.m b/SPDY/SPDYFrameEncoder.m new file mode 100644 index 0000000..6158dbd --- /dev/null +++ b/SPDY/SPDYFrameEncoder.m @@ -0,0 +1,329 @@ +// +// SPDYFrameEncoder.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "SPDYFrameEncoder.h" +#import "SPDYHeaderBlockCompressor.h" + +#define MAX_HEADER_BLOCK_LENGTH 16384 +#define MAX_COMPRESSED_HEADER_BLOCK_LENGTH 16384 + +@interface SPDYFrameEncoder () +- (void)_encodeHeaders:(NSDictionary *)dictionary; +@end + +@implementation SPDYFrameEncoder +{ + SPDYHeaderBlockCompressor *_compressor; + NSUInteger _encodedHeadersLength; + NSUInteger _compressedLength; + uint8_t *_encodedHeaders; + uint8_t *_compressed; +} + +- (id)initWithDelegate:(id)delegate headerCompressionLevel:(NSUInteger)headerCompressionLevel +{ + self = [super init]; + if (self) { + _delegate = delegate; + + _compressor = [[SPDYHeaderBlockCompressor alloc] initWithCompressionLevel:headerCompressionLevel]; + _encodedHeaders = malloc(sizeof(uint8_t) * MAX_COMPRESSED_HEADER_BLOCK_LENGTH); + _compressed = malloc(sizeof(uint8_t) * MAX_COMPRESSED_HEADER_BLOCK_LENGTH); + _encodedHeadersLength = 0; + _compressedLength = 0; + } + return self; +} + +- (void)dealloc +{ + free(_encodedHeaders); + free(_compressed); +} + +- (bool)encodeDataFrame:(SPDYDataFrame *)dataFrame +{ + NSMutableData *encodedData = [[NSMutableData alloc] initWithCapacity:8]; + + uint32_t streamId = htonl(dataFrame.streamId); + uint32_t flags = SPDY_DATA_FLAG_FIN * dataFrame.last; + uint32_t flags_length = htonl(flags << 24 | (uint32_t)dataFrame.data.length); + + [encodedData appendBytes:&streamId length:4]; + [encodedData appendBytes:&flags_length length:4]; + + [_delegate didEncodeData:encodedData frameEncoder:self]; + [_delegate didEncodeData:dataFrame.data frameEncoder:self]; + return YES; +} + +- (bool)encodeSynStreamFrame:(SPDYSynStreamFrame *)synStreamFrame +{ + [self _encodeHeaders:synStreamFrame.headers]; + + NSMutableData *encodedData = [[NSMutableData alloc] initWithCapacity:18]; + NSData *encodedHeaders = [[NSData alloc] initWithBytes:_compressed length:_compressedLength]; + + uint8_t control = 0x80; + uint8_t version = 3; + uint16_t type = htons(SPDY_SYN_STREAM_FRAME); + uint32_t flags = SPDY_FLAG_FIN * synStreamFrame.last | SPDY_FLAG_UNIDIRECTIONAL * synStreamFrame.unidirectional; + uint32_t flags_length = htonl(flags << 24 | 10 + _compressedLength); + uint32_t streamId = htonl(synStreamFrame.streamId); + uint32_t assocStreamId = htonl(synStreamFrame.associatedToStreamId); + uint16_t priority_slot = htons((uint16_t)synStreamFrame.priority << 13); + + [encodedData appendBytes:&control length:1]; + [encodedData appendBytes:&version length:1]; + [encodedData appendBytes:&type length:2]; + [encodedData appendBytes:&flags_length length:4]; + [encodedData appendBytes:&streamId length:4]; + [encodedData appendBytes:&assocStreamId length:4]; + [encodedData appendBytes:&priority_slot length:2]; + + [_delegate didEncodeData:encodedData frameEncoder:self]; + [_delegate didEncodeData:encodedHeaders frameEncoder:self]; + return YES; +} + +- (bool)encodeSynReplyFrame:(SPDYSynReplyFrame *)synReplyFrame +{ + [self _encodeHeaders:synReplyFrame.headers]; + + NSMutableData *encodedData = [[NSMutableData alloc] initWithCapacity:12]; + NSData *encodedHeaders = [[NSData alloc] initWithBytes:_compressed length:_compressedLength]; + + uint8_t control = 0x80; + uint8_t version = 3; + uint16_t type = htons(SPDY_SYN_REPLY_FRAME); + uint32_t flags = SPDY_FLAG_FIN * synReplyFrame.last; + uint32_t flags_length = htonl(flags << 24 | 4 + _compressedLength); + uint32_t streamId = htonl(synReplyFrame.streamId); + + [encodedData appendBytes:&control length:1]; + [encodedData appendBytes:&version length:1]; + [encodedData appendBytes:&type length:2]; + [encodedData appendBytes:&flags_length length:4]; + [encodedData appendBytes:&streamId length:4]; + + [_delegate didEncodeData:encodedData frameEncoder:self]; + [_delegate didEncodeData:encodedHeaders frameEncoder:self]; + return YES; +} + +- (bool)encodeRstStreamFrame:(SPDYRstStreamFrame *)rstStreamFrame +{ + NSMutableData *encodedData = [[NSMutableData alloc] initWithCapacity:16]; + + uint8_t control = 0x80; + uint8_t version = 3; + uint16_t type = htons(SPDY_RST_STREAM_FRAME); + uint32_t flags_length = htonl(8); // no flags + uint32_t streamId = htonl(rstStreamFrame.streamId); + uint32_t statusCode = htonl(rstStreamFrame.statusCode); + + [encodedData appendBytes:&control length:1]; + [encodedData appendBytes:&version length:1]; + [encodedData appendBytes:&type length:2]; + [encodedData appendBytes:&flags_length length:4]; + [encodedData appendBytes:&streamId length:4]; + [encodedData appendBytes:&statusCode length:4]; + + [_delegate didEncodeData:encodedData frameEncoder:self]; + return YES; +} + +- (bool)encodeSettingsFrame:(SPDYSettingsFrame *)settingsFrame +{ + uint32_t numEntries = 0; + + SPDY_SETTINGS_ITERATOR(i) { + if (settingsFrame.settings[i].set) { + numEntries++; + } + } + + NSMutableData *encodedData = [[NSMutableData alloc] initWithCapacity:(12 + 8 * numEntries)]; + + uint8_t control = 0x80; + uint8_t version = 3; + uint16_t type = htons(SPDY_SETTINGS_FRAME); + uint32_t flags = SPDY_SETTINGS_FLAG_CLEAR_SETTINGS * settingsFrame.clearSettings; + uint32_t flags_length = htonl(flags << 24 | 4 + 8 * numEntries); + numEntries = htonl(numEntries); + + [encodedData appendBytes:&control length:1]; + [encodedData appendBytes:&version length:1]; + [encodedData appendBytes:&type length:2]; + [encodedData appendBytes:&flags_length length:4]; + [encodedData appendBytes:&numEntries length:4]; + + SPDY_SETTINGS_ITERATOR(i) { + if (settingsFrame.settings[i].set) { + uint32_t flags_entryId = htonl((uint32_t)settingsFrame.settings[i].flags << 24 | i); + uint32_t entryValue = htonl(settingsFrame.settings[i].value); + [encodedData appendBytes:&flags_entryId length:4]; + [encodedData appendBytes:&entryValue length:4]; + } + } + + [_delegate didEncodeData:encodedData frameEncoder:self]; + return YES; +} + +- (bool)encodePingFrame:(SPDYPingFrame *)pingFrame +{ + NSMutableData *encodedData = [[NSMutableData alloc] initWithCapacity:12]; + + uint8_t control = 0x80; + uint8_t version = 3; + uint16_t type = htons(SPDY_PING_FRAME); + uint32_t flags_length = htonl(4); // no flags + uint32_t pingId = htonl(pingFrame.id); + + [encodedData appendBytes:&control length:1]; + [encodedData appendBytes:&version length:1]; + [encodedData appendBytes:&type length:2]; + [encodedData appendBytes:&flags_length length:4]; + [encodedData appendBytes:&pingId length:4]; + + [_delegate didEncodeData:encodedData frameEncoder:self]; + return YES; +} + +- (bool)encodeGoAwayFrame:(SPDYGoAwayFrame *)goAwayFrame +{ + NSMutableData *encodedData = [[NSMutableData alloc] initWithCapacity:1]; + + uint8_t control = 0x80; + uint8_t version = 3; + uint16_t type = htons(SPDY_GOAWAY_FRAME); + uint32_t flags_length = htonl(8); // no flags + uint32_t lastGoodStreamId = htonl(goAwayFrame.lastGoodStreamId); + uint32_t statusCode = htonl(goAwayFrame.statusCode); + + [encodedData appendBytes:&control length:1]; + [encodedData appendBytes:&version length:1]; + [encodedData appendBytes:&type length:2]; + [encodedData appendBytes:&flags_length length:4]; + [encodedData appendBytes:&lastGoodStreamId length:4]; + [encodedData appendBytes:&statusCode length:4]; + + [_delegate didEncodeData:encodedData frameEncoder:self]; + return YES; +} + +- (bool)encodeHeadersFrame:(SPDYHeadersFrame *)headersFrame +{ + [self _encodeHeaders:headersFrame.headers]; + + NSMutableData *encodedData = [[NSMutableData alloc] initWithCapacity:12]; + NSData *encodedHeaders = [[NSData alloc] initWithBytes:_compressed length:_compressedLength]; + + uint8_t control = 0x80; + uint8_t version = 3; + uint16_t type = htons(SPDY_HEADERS_FRAME); + uint32_t flags = SPDY_FLAG_FIN * headersFrame.last; + uint32_t flags_length = htonl(flags << 24 | 4 + _compressedLength); + uint32_t streamId = htonl(headersFrame.streamId); + + [encodedData appendBytes:&control length:1]; + [encodedData appendBytes:&version length:1]; + [encodedData appendBytes:&type length:2]; + [encodedData appendBytes:&flags_length length:4]; + [encodedData appendBytes:&streamId length:4]; + + [_delegate didEncodeData:encodedData frameEncoder:self]; + [_delegate didEncodeData:encodedHeaders frameEncoder:self]; + return YES; +} + +- (bool)encodeWindowUpdateFrame:(SPDYWindowUpdateFrame *)windowUpdateFrame +{ + NSMutableData *encodedData = [[NSMutableData alloc] initWithCapacity:16]; + + uint8_t control = 0x80; + uint8_t version = 3; + uint16_t type = htons(SPDY_WINDOW_UPDATE_FRAME); + uint32_t flags_length = htonl(8); // no flags + uint32_t streamId = htonl(windowUpdateFrame.streamId); + uint32_t windowDelta = htonl(windowUpdateFrame.deltaWindowSize); + + [encodedData appendBytes:&control length:1]; + [encodedData appendBytes:&version length:1]; + [encodedData appendBytes:&type length:2]; + [encodedData appendBytes:&flags_length length:4]; + [encodedData appendBytes:&streamId length:4]; + [encodedData appendBytes:&windowDelta length:4]; + + [_delegate didEncodeData:encodedData frameEncoder:self]; + return YES; +} + +#pragma mark private methods + +- (void)_encodeHeaders:(NSDictionary *)headers +{ + *((uint32_t *)_encodedHeaders) = htonl((uint32_t)headers.count); + _encodedHeadersLength = 4; + _compressedLength = 0; + + for (NSString *headerName in headers) { + NSUInteger headerNameLength = headerName.length; + + *((uint32_t *)(_encodedHeaders + _encodedHeadersLength)) = htonl((uint32_t)headerNameLength); + _encodedHeadersLength += 4; + + [headerName getBytes:(_encodedHeaders + _encodedHeadersLength) + maxLength:(MAX_HEADER_BLOCK_LENGTH - _encodedHeadersLength) + usedLength:NULL + encoding:NSUTF8StringEncoding + options:NSStringEncodingConversionAllowLossy + range:NSMakeRange(0, headerNameLength) + remainingRange:NULL]; + _encodedHeadersLength += headerNameLength; + + NSString *headerValue; + + if ([headers[headerName] isKindOfClass:[NSString class]]) { + headerValue = headers[headerName]; + } else if ([headers[headerName] isKindOfClass:[NSArray class]]) { + headerValue = [headers[headerName] componentsJoinedByString:@"\0"]; + } + + NSUInteger headerValueLength = headerValue.length; + *((uint32_t *)(_encodedHeaders + _encodedHeadersLength)) = htonl((uint32_t)headerValueLength); + _encodedHeadersLength += 4; + + [headerValue getBytes:(_encodedHeaders + _encodedHeadersLength) + maxLength:(MAX_HEADER_BLOCK_LENGTH - _encodedHeadersLength) + usedLength:NULL + encoding:NSUTF8StringEncoding + options:NSStringEncodingConversionAllowLossy + range:NSMakeRange(0, headerValueLength) + remainingRange:NULL]; + _encodedHeadersLength += headerValueLength; + } + + NSError *error = nil; + + _compressedLength = [_compressor deflate:_encodedHeaders + availIn:_encodedHeadersLength + outputBuffer:_compressed + availOut:MAX_COMPRESSED_HEADER_BLOCK_LENGTH + error:&error]; +} + +@end diff --git a/SPDY/SPDYHeaderBlockCompressor.h b/SPDY/SPDYHeaderBlockCompressor.h new file mode 100644 index 0000000..79090fc --- /dev/null +++ b/SPDY/SPDYHeaderBlockCompressor.h @@ -0,0 +1,17 @@ +// +// SPDYHeaderBlockCompressor.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import + +@interface SPDYHeaderBlockCompressor : NSObject +- (id)initWithCompressionLevel:(NSUInteger)compressionLevel; +- (NSUInteger)deflate:(uint8_t *)inputBuffer availIn:(NSUInteger)inputLength outputBuffer:(uint8_t *)outputBuffer availOut:(NSUInteger)outputLength error:(NSError **)pError; +@end diff --git a/SPDY/SPDYHeaderBlockCompressor.m b/SPDY/SPDYHeaderBlockCompressor.m new file mode 100644 index 0000000..0caac7d --- /dev/null +++ b/SPDY/SPDYHeaderBlockCompressor.m @@ -0,0 +1,86 @@ +// +// SPDYHeaderBlockCompressor.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "SPDYHeaderBlockCompressor.h" +#import "SPDYError.h" +#import "SPDYZLibCommon.h" + +// See https://groups.google.com/group/spdy-dev/browse_thread/thread/dfaf498542fac792 +#define ZLIB_COMPRESSION_LEVEL 9 +#define ZLIB_WINDOW_SIZE 11 +#define ZLIB_MEMORY_LEVEL 1 + +@implementation SPDYHeaderBlockCompressor +{ + z_stream _zlibStream; + int _zlibStreamStatus; +} + +- (id)init +{ + return [self initWithCompressionLevel:ZLIB_COMPRESSION_LEVEL]; +} + +- (id)initWithCompressionLevel:(NSUInteger)compressionLevel +{ + self = [super init]; + if (self) { + bzero(&_zlibStream, sizeof(_zlibStream)); + + _zlibStream.zalloc = Z_NULL; + _zlibStream.zfree = Z_NULL; + _zlibStream.opaque = Z_NULL; + + _zlibStream.avail_in = 0; + _zlibStream.next_in = Z_NULL; + + _zlibStreamStatus = deflateInit2(&_zlibStream, compressionLevel, Z_DEFLATED, ZLIB_WINDOW_SIZE, ZLIB_MEMORY_LEVEL, Z_DEFAULT_STRATEGY); + NSAssert(_zlibStreamStatus == Z_OK, @"unable to initialize zlib stream"); + + _zlibStreamStatus = deflateSetDictionary(&_zlibStream, kSPDYDict, sizeof(kSPDYDict)); + NSAssert(_zlibStreamStatus == Z_OK, @"unable to set zlib dictionary"); + } + return self; +} + +- (void)dealloc +{ + (void)deflateEnd(&_zlibStream); +} + +// Always consumes ALL available input or sets an error, and returns the number of bytes written to the output buffer. +- (NSUInteger)deflate:(uint8_t *)inputBuffer availIn:(NSUInteger)inputLength outputBuffer:(uint8_t *)outputBuffer availOut:(NSUInteger)outputLength error:(NSError **)pError +{ + if (_zlibStreamStatus != Z_OK) { + if (pError) *pError = SPDY_CODEC_ERROR(SDPYHeaderBlockEncodingError, @"invalid zlib stream state"); + return 0; + } + + _zlibStream.next_in = inputBuffer; + _zlibStream.avail_in = (uInt)inputLength; + + _zlibStream.next_out = outputBuffer; + _zlibStream.avail_out = (uInt)outputLength; + + _zlibStreamStatus = deflate(&_zlibStream, Z_SYNC_FLUSH); + + if (_zlibStreamStatus != Z_OK && pError) { + *pError = SPDY_CODEC_ERROR(SDPYHeaderBlockEncodingError, @"error compressing header block"); + } + + return _zlibStream.next_out - outputBuffer; +} + +@end diff --git a/SPDY/SPDYHeaderBlockDecompressor.h b/SPDY/SPDYHeaderBlockDecompressor.h new file mode 100644 index 0000000..870d97c --- /dev/null +++ b/SPDY/SPDYHeaderBlockDecompressor.h @@ -0,0 +1,17 @@ +// +// SPDYHeaderBlockDecompressor.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import + + +@interface SPDYHeaderBlockDecompressor : NSObject +- (NSUInteger)inflate:(uint8_t *)inputBuffer availIn:(NSUInteger)inputLength outputBuffer:(uint8_t *)outputBuffer availOut:(NSUInteger)outputLength error:(NSError **)pError; +@end diff --git a/SPDY/SPDYHeaderBlockDecompressor.m b/SPDY/SPDYHeaderBlockDecompressor.m new file mode 100644 index 0000000..d1826bb --- /dev/null +++ b/SPDY/SPDYHeaderBlockDecompressor.m @@ -0,0 +1,97 @@ +// +// SPDYHeaderBlockDecompressor.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "SPDYHeaderBlockDecompressor.h" +#import "SPDYError.h" +#import "SPDYZLibCommon.h" + +@implementation SPDYHeaderBlockDecompressor +{ + z_stream _zlibStream; + int _zlibStreamStatus; +} + +- (id)init +{ + self = [super init]; + if (self) { + bzero(&_zlibStream, sizeof(_zlibStream)); + + _zlibStream.zalloc = Z_NULL; + _zlibStream.zfree = Z_NULL; + _zlibStream.opaque = Z_NULL; + + _zlibStream.avail_in = 0; + _zlibStream.next_in = Z_NULL; + + _zlibStreamStatus = inflateInit(&_zlibStream); + NSAssert(_zlibStreamStatus == Z_OK, @"unable to initialize zlib stream"); + } + return self; +} + +- (void)dealloc +{ + inflateEnd(&_zlibStream); +} + +// Always consumes ALL available input or sets an error, and returns the number of bytes written to the output buffer. +- (NSUInteger)inflate:(uint8_t *)inputBuffer availIn:(NSUInteger)inputLength outputBuffer:(uint8_t *)outputBuffer availOut:(NSUInteger)outputLength error:(NSError **)pError +{ + if (_zlibStreamStatus != Z_OK) { + if (pError) *pError = SPDY_CODEC_ERROR(SDPYHeaderBlockDecodingError, @"invalid zlib stream state"); + return 0; + } + + _zlibStream.next_in = inputBuffer; + _zlibStream.avail_in = (uInt)inputLength; + + _zlibStream.next_out = outputBuffer; + _zlibStream.avail_out = (uInt)outputLength; + + while (_zlibStream.avail_in > 0 && !*pError) { + _zlibStreamStatus = inflate(&_zlibStream, Z_SYNC_FLUSH); + + switch (_zlibStreamStatus) { + case Z_NEED_DICT: + // We can't set the dictionary ahead of time due to zlib funkiness. + _zlibStreamStatus = inflateSetDictionary(&_zlibStream, kSPDYDict, sizeof(kSPDYDict)); + NSAssert(_zlibStreamStatus == Z_OK, @"unable to set zlib dictionary"); + break; + + case Z_STREAM_END: + break; + + case Z_BUF_ERROR: + case Z_OK: + // For simplicity's sake, if avail_out == 0, we treat the header block + // as too large for this implementation to handle. + if (_zlibStream.avail_out == 0) { + *pError = SPDY_CODEC_ERROR(SDPYHeaderBlockDecodingError, @"header block is too large"); + } + break; + + case Z_STREAM_ERROR: + case Z_DATA_ERROR: + case Z_MEM_ERROR: + *pError = SPDY_CODEC_ERROR(SDPYHeaderBlockDecodingError, @"error decompressing header block"); + break; + } + } + + return _zlibStream.next_out - outputBuffer; +} + +@end diff --git a/SPDY/SPDYLogger.h b/SPDY/SPDYLogger.h new file mode 100644 index 0000000..64c7ab5 --- /dev/null +++ b/SPDY/SPDYLogger.h @@ -0,0 +1,21 @@ +// +// SPDYLogger.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +typedef enum { + SPDYLogLevelError = 0, + SPDYLogLevelWarning, + SPDYLogLevelInfo, + SPDYLogLevelDebug +} SPDYLogLevel; + +@protocol SPDYLogger +- (void)log:(NSString *)message atLevel:(SPDYLogLevel)logLevel; +@end diff --git a/SPDY/SPDYOrigin.h b/SPDY/SPDYOrigin.h new file mode 100644 index 0000000..5cbd122 --- /dev/null +++ b/SPDY/SPDYOrigin.h @@ -0,0 +1,30 @@ +// +// SPDYOrigin.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import + +/** + Representation for RFC 6454 origin. + + http://www.ietf.org/rfc/rfc6454.txt + */ +@interface SPDYOrigin : NSObject +@property (nonatomic, readonly) NSString *scheme; +@property (nonatomic, readonly) NSString *host; +@property (nonatomic, readonly) in_port_t port; + +- (id)initWithString:(NSString *)urlString error:(NSError **)pError; +- (id)initWithURL:(NSURL *)url error:(NSError **)pError; +- (id)initWithScheme:(NSString *)scheme + host:(NSString *)host + port:(in_port_t)port + error:(NSError **)pError; +@end diff --git a/SPDY/SPDYOrigin.m b/SPDY/SPDYOrigin.m new file mode 100644 index 0000000..69080d1 --- /dev/null +++ b/SPDY/SPDYOrigin.m @@ -0,0 +1,124 @@ +// +// SPDYOrigin.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "SPDYOrigin.h" + +@interface SPDYOrigin () +- (id)initCopyWithScheme:(NSString *)scheme + host:(NSString *)host + port:(in_port_t)port + serialization:(NSString *)serialization; +@end + +@implementation SPDYOrigin +{ + NSString *_serialization; +} + +- (id)initWithString:(NSString *)urlString error:(NSError **)pError +{ + NSURL *url = [[NSURL alloc] initWithString:urlString]; + return [self initWithURL:url error:pError]; +} + +- (id)initWithURL:(NSURL *)url error:(NSError **)pError +{ + return [self initWithScheme:url.scheme + host:url.host + port:url.port.unsignedShortValue + error:pError]; +} + +- (id)initWithScheme:(NSString *)scheme + host:(NSString *)host + port:(in_port_t)port + error:(NSError **)pError +{ + self = [super init]; + if (self) { + _scheme = [scheme lowercaseString]; + if (![_scheme isEqualToString:@"http"] && ![_scheme isEqualToString:@"https"]) { + if (pError) { + NSString *message = [[NSString alloc] initWithFormat:@"unsupported scheme (%@) - only http and https are supported", scheme]; + NSDictionary *info = @{ NSLocalizedDescriptionKey: message }; + *pError = [[NSError alloc] initWithDomain:NSURLErrorDomain + code:NSURLErrorBadURL + userInfo:info]; + } + return nil; + } + + if (host) { + _host = [host lowercaseString]; + } else { + if (pError) { + NSString *message = @"host must be specified"; + NSDictionary *info = @{ NSLocalizedDescriptionKey: message }; + *pError = [[NSError alloc] initWithDomain:NSURLErrorDomain + code:NSURLErrorBadURL + userInfo:info]; + } + return nil; + } + + if (port == 0) { + _port = [_scheme isEqualToString:@"http"] ? 80 : 443; + } else { + _port = port; + } + + _serialization = [[NSString alloc] initWithFormat:@"%@://%@:%uh", _scheme, _host, _port]; + } + return self; +} + +- (id)initCopyWithScheme:(NSString *)scheme + host:(NSString *)host + port:(in_port_t)port + serialization:(NSString *)serialization +{ + self = [super init]; + if (self) { + _scheme = scheme; + _host = host; + _port = port; + _serialization = serialization; + } + return self; +} + +- (NSUInteger)hash +{ + return [_serialization hash]; +} + +- (BOOL)isEqual:(id)object +{ + return self == object || ( + [object isMemberOfClass:[self class]] && + [_serialization isEqualToString:((SPDYOrigin *)object)->_serialization] + ); +} + +- (id)copyWithZone:(NSZone *)zone +{ + SPDYOrigin *copy = [[SPDYOrigin allocWithZone:zone] initCopyWithScheme:[_scheme copyWithZone:zone] + host:[_host copyWithZone:zone] + port:_port + serialization:[_serialization copyWithZone:zone]]; + return copy; +} + +@end diff --git a/SPDY/SPDYProtocol.h b/SPDY/SPDYProtocol.h new file mode 100644 index 0000000..d2f1e61 --- /dev/null +++ b/SPDY/SPDYProtocol.h @@ -0,0 +1,139 @@ +// +// SPDYProtocol.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import + +extern NSString *const SPDYOriginRegisteredNotification; +extern NSString *const SPDYOriginUnregisteredNotification; + +@class SPDYConfiguration; + +@protocol SPDYLogger; +@protocol SPDYTLSTrustEvaluator; + +/** + Client implementation of the SPDY/3.1 draft protocol. +*/ +@interface SPDYProtocol : NSURLProtocol + +/** + Set configuration options to be used for all future SPDY sessions. +*/ ++ (void)setConfiguration:(SPDYConfiguration *)configuration; + +/** + Register an object that implements @proto(SPDYLogger) to receive log + output. + + Note that log messages are dispatched asynchronously. + */ ++ (void)setLogger:(id)logger; + +/** + Register an object to perform additional evaluation of TLS certificates. + + Methods on this object will be called from socket threads and should, + therefore, be threadsafe. +*/ ++ (void)setTLSTrustEvaluator:(id)evaluator; + +/** + Accessor for current TLS trust evaluation object. +*/ ++ (id)sharedTLSTrustEvaluator; + +@end + +/** + Protocol implementation intended for use with NSURLSession. + + Currently identical to SDPYProtocol, but potential future + NSURLSession-specific features will be present in this subclass only +*/ +@interface SPDYURLSessionProtocol : SPDYProtocol +@end + + +/** + Protocol implementation intended for use with NSURLConnection. +*/ +@interface SPDYURLConnectionProtocol : SPDYProtocol + +/** + Register an endpoint with SPDY. The protocol will handle all future + communication for that endpoint originating in the NSURL stack. + + @param origin The scheme-host-port tuple for the endpoint, in URL + format, e.g. @"https://twitter.com:443" + */ ++ (void)registerOrigin:(NSString *)origin; + +/** + Unregister an endpoint with SPDY. The protocol will stop handling + communication for the endpoint, though existing connections will be + maintained until completion/termination. + + @param origin The scheme-host-port tuple for the endpoint, in URL + format, e.g. @"https://twitter.com:443" + */ ++ (void)unregisterOrigin:(NSString *)origin; + +/** + Unregister all endpoints from SPDY. The protocol will stop handling + any communication, though existing connections will be maintained + until completion/termination. + */ ++ (void)unregisterAll; + +@end + +/** + Configuration options for a SPDYSession. + + When a SPDY session is opened, a copy of the configuration object + is made - you cannot modify the configuration of a session after it + has been opened. +*/ +@interface SPDYConfiguration : NSObject + ++ (SPDYConfiguration *)defaultConfiguration; + +/** + Initial session window size for client flow control. + + Default is 10MB. If your application is receiving large responses and + has ample memory available, it won't hurt to make this even larger. +*/ +@property NSUInteger sessionReceiveWindow; + +/** + Initial stream window size for client flow control. + + Default is 10MB. +*/ +@property NSUInteger streamReceiveWindow; + +/** + ZLib compression level to use for headers. + + Default is 9, which is appropriate for most cases. To disable header + compression set this to 0. +*/ +@property NSUInteger headerCompressionLevel; + +/** + Enable or disable sending minor protocol version with settings id 0. + + Default is enabled. +*/ +@property BOOL enableSettingsMinorVersion; + +@end diff --git a/SPDY/SPDYProtocol.m b/SPDY/SPDYProtocol.m new file mode 100644 index 0000000..6d16550 --- /dev/null +++ b/SPDY/SPDYProtocol.m @@ -0,0 +1,245 @@ +// +// SPDYProtocol.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "SPDYProtocol.h" +#import "SPDYCommonLogger.h" +#import "SPDYOrigin.h" +#import "SPDYSession.h" +#import "SPDYSessionManager.h" +#import "SPDYTLSTrustEvaluator.h" + +NSString *const SPDYStreamErrorDomain = @"SPDYStreamErrorDomain"; +NSString *const SPDYSessionErrorDomain = @"SPDYSessionErrorDomain"; +NSString *const SPDYCodecErrorDomain = @"SPDYCodecErrorDomain"; +NSString *const SPDYSocketErrorDomain = @"SPDYSocketErrorDomain"; +NSString *const SPDYOriginRegisteredNotification = @"SPDYOriginRegisteredNotification"; +NSString *const SPDYOriginUnregisteredNotification = @"SPDYOriginUnregisteredNotification"; + +static NSString *const kSPDYOverride = @"SPDYOverride"; +static char *const SPDYOriginQueue = "com.twitter.SPDYOriginQueue"; +static char *const SPDYTrustQueue = "com.twitter.SPDYTrustQueue"; + +@implementation SPDYProtocol +{ + SPDYSession *_session; +} + +static dispatch_once_t initTrust; +static dispatch_queue_t trustQueue; +static id trustEvaluator; + ++ (void)initialize +{ + dispatch_once(&initTrust, ^{ + trustQueue = dispatch_queue_create(SPDYTrustQueue, DISPATCH_QUEUE_CONCURRENT); + }); + +#ifdef DEBUG + SPDY_WARNING(@"loaded DEBUG build of SPDY framework"); +#endif +} + ++ (void)setConfiguration:(SPDYConfiguration *)configuration +{ + [SPDYSessionManager setConfiguration:configuration]; +} + ++ (void)setLogger:(id)logger +{ + [SPDYCommonLogger setLogger:logger]; +} + ++ (void)setTLSTrustEvaluator:(id)evaluator +{ + SPDY_INFO(@"register trust evaluator: %@", evaluator); + dispatch_barrier_async(trustQueue, ^{ + trustEvaluator = evaluator; + }); +} + ++ (id)sharedTLSTrustEvaluator +{ + __block id evaluator; + dispatch_sync(trustQueue, ^{ + evaluator = trustEvaluator; + }); + return evaluator; +} + +#pragma mark NSURLProtocol implementation + ++ (BOOL)canInitWithRequest:(NSURLRequest *)request +{ + NSString *scheme = request.URL.scheme.lowercaseString; + if (![scheme isEqualToString:@"http"] && ![scheme isEqualToString:@"https"]) { + return NO; + } + + NSNumber *override = [SPDYProtocol propertyForKey:kSPDYOverride inRequest:request]; + return override == nil || override.boolValue; +} + ++ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request +{ + return request; +} + +- (void)startLoading +{ + NSURLRequest *request = self.request; + SPDY_INFO(@"start loading %@", request.URL.absoluteString); + + NSError *error; + _session = [SPDYSessionManager sessionForURL:request.URL error:&error]; + if (!_session) { + [self.client URLProtocol:self didFailWithError:error]; + } else { + [_session issueRequest:self]; + } +} + +- (void)stopLoading +{ + SPDY_INFO(@"stop loading %@", self.request.URL.absoluteString); + + if (_session) { + [_session cancelRequest:self]; + } +} + +@end + +#pragma mark NSURLSession implementation + +@implementation SPDYURLSessionProtocol +@end + + +//__attribute__((constructor)) +//static void registerSPDYURLConnectionProtocol() { +// @autoreleasepool { +// [NSURLProtocol registerClass:[SPDYURLConnectionProtocol class]]; +// } +//} + +#pragma mark NSURLConnection implementation + +@implementation SPDYURLConnectionProtocol +static dispatch_once_t initialized; +static dispatch_queue_t originQueue; +static NSMutableSet *origins; + ++ (void)load +{ + // +[NSURLProtocol registerClass] is not threadsafe, so register before + // requests start getting made. + @autoreleasepool { + [NSURLProtocol registerClass:self]; + } +} + ++ (void)initialize +{ + dispatch_once(&initialized, ^{ + origins = [[NSMutableSet alloc] init]; + originQueue = dispatch_queue_create(SPDYOriginQueue, DISPATCH_QUEUE_CONCURRENT); + }); +} + ++ (void)registerOrigin:(NSString *)originString +{ + SPDYOrigin *origin = [[SPDYOrigin alloc] initWithString:originString error:nil]; + SPDY_INFO(@"register origin: %@", origin); + dispatch_barrier_async(originQueue, ^{ + [origins addObject:origin]; + [[NSNotificationCenter defaultCenter] postNotificationName:SPDYOriginRegisteredNotification object:nil userInfo:@{ @"origin": originString }]; + SPDY_DEBUG(@"origin registered: %@", origin); + }); +} + ++ (void)unregisterOrigin:(NSString *)originString +{ + SPDYOrigin *origin = [[SPDYOrigin alloc] initWithString:originString error:nil]; + dispatch_barrier_async(originQueue, ^{ + if ([origins containsObject:origin]) { + [origins removeObject:origin]; + [[NSNotificationCenter defaultCenter] postNotificationName:SPDYOriginUnregisteredNotification object:nil userInfo:@{ @"origin": originString }]; + SPDY_DEBUG(@"origin unregistered: %@", origin); + } + }); +} + ++ (void)unregisterAll +{ + dispatch_barrier_async(originQueue, ^{ + [origins removeAllObjects]; + [[NSNotificationCenter defaultCenter] postNotificationName:SPDYOriginUnregisteredNotification object:nil userInfo:@{ @"origin": @"*" }]; + SPDY_DEBUG(@"unregistered all origins"); + }); +} + +#pragma mark NSURLProtocol implementation + ++ (BOOL)canInitWithRequest:(NSURLRequest *)request +{ + if (![super canInitWithRequest:request]) { + return NO; + } + + SPDYOrigin *origin = [[SPDYOrigin alloc] initWithURL:request.URL error:nil]; + if (!origin) { + return NO; + } + + __block bool originRegistered; + dispatch_sync(originQueue, ^{ + originRegistered = [origins containsObject:origin]; + }); + return originRegistered; +} + +@end + +#pragma mark Configuration + +@implementation SPDYConfiguration + +static SPDYConfiguration *defaultConfiguration; + ++ (void)initialize +{ + defaultConfiguration = [[SPDYConfiguration alloc] init]; + defaultConfiguration.headerCompressionLevel = 9; + defaultConfiguration.sessionReceiveWindow = 10485760; + defaultConfiguration.streamReceiveWindow = 10485760; + defaultConfiguration.enableSettingsMinorVersion = YES; +} + ++ (SPDYConfiguration *)defaultConfiguration +{ + return [defaultConfiguration copy]; +} + +- (id)copyWithZone:(NSZone *)zone +{ + SPDYConfiguration *copy = [[SPDYConfiguration allocWithZone:zone] init]; + copy.headerCompressionLevel = _headerCompressionLevel; + copy.sessionReceiveWindow = _sessionReceiveWindow; + copy.streamReceiveWindow = _streamReceiveWindow; + copy.enableSettingsMinorVersion = _enableSettingsMinorVersion; + return copy; +} + +@end diff --git a/SPDY/SPDYSession.h b/SPDY/SPDYSession.h new file mode 100644 index 0000000..270d9b7 --- /dev/null +++ b/SPDY/SPDYSession.h @@ -0,0 +1,25 @@ +// +// SPDYSession.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import + +@class SPDYProtocol; +@class SPDYConfiguration; +@class SPDYOrigin; + +@interface SPDYSession : NSObject +@property (nonatomic, readonly) SPDYOrigin *origin; +@property (nonatomic, readonly) bool isOpen; +- (id)initWithOrigin:(SPDYOrigin *)origin configuration:(SPDYConfiguration *)configuration error:(NSError **)pError; +- (void)issueRequest:(SPDYProtocol *)protocol; +- (void)cancelRequest:(SPDYProtocol *)protocol; +- (void)close; +@end diff --git a/SPDY/SPDYSession.m b/SPDY/SPDYSession.m new file mode 100644 index 0000000..442ff54 --- /dev/null +++ b/SPDY/SPDYSession.m @@ -0,0 +1,866 @@ +// +// SPDYSession.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "NSURLRequest+SPDYURLRequest.h" +#import "SPDYCommonLogger.h" +#import "SPDYFrameDecoder.h" +#import "SPDYFrameEncoder.h" +#import "SPDYOrigin.h" +#import "SPDYProtocol.h" +#import "SPDYSession.h" +#import "SPDYSessionManager.h" +#import "SPDYSettingsStore.h" +#import "SPDYSocket.h" +#import "SPDYStream.h" +#import "SPDYStreamManager.h" +#import "SPDYTLSTrustEvaluator.h" + +// The input buffer should be more than twice MAX_CHUNK_LENGTH and +// MAX_COMPRESSED_HEADER_BLOCK_LENGTH to avoid having to resize the +// buffer. +#define DEFAULT_WINDOW_SIZE 65536 +#define INITIAL_INPUT_BUFFER_SIZE 65536 +#define LOCAL_MAX_CONCURRENT_STREAMS 0 +#define REMOTE_MAX_CONCURRENT_STREAMS INT32_MAX +#define ENABLE_SESSION_FLOW_CONTROL 1 + +@interface SPDYSession () +@property (nonatomic, readonly) SPDYStreamId nextStreamId; +- (void)_sendSynStream:(SPDYStream *)stream streamId:(SPDYStreamId)streamId closeLocal:(bool)close; +- (void)_sendData:(SPDYStream *)stream; +- (void)_sendWindowUpdate:(NSUInteger)deltaWindowSize streamId:(SPDYStreamId)streamId; +- (void)_sendPingResponse:(SPDYPingFrame *)pingFrame; +- (void)_sendRstStream:(SPDYStreamStatus)status streamId:(SPDYStreamId)streamId; +- (void)_sendGoAway:(SPDYSessionStatus)status; +@end + +@implementation SPDYSession +{ + SPDYSocket *_socket; + SPDYFrameDecoder *_frameDecoder; + SPDYFrameEncoder *_frameEncoder; + SPDYStreamManager *_activeStreams; + SPDYStreamManager *_inactiveStreams; + NSMutableData *_inputBuffer; + + SPDYStreamId _lastGoodStreamId; + SPDYStreamId _nextStreamId; + NSUInteger _bufferReadIndex; + NSUInteger _bufferWriteIndex; + NSUInteger _initialSendWindowSize; + NSUInteger _initialReceiveWindowSize; + NSUInteger _sessionSendWindowSize; + NSUInteger _sessionReceiveWindowSize; + NSUInteger _localMaxConcurrentStreams; + NSUInteger _remoteMaxConcurrentStreams; + time_t _lastSocketActivity; + bool _enableSettingsMinorVersion; + bool _receivedGoAwayFrame; + bool _sentGoAwayFrame; +} + +- (id)initWithOrigin:(SPDYOrigin *)origin configuration:(SPDYConfiguration *)configuration error:(NSError **)pError +{ + NSParameterAssert(origin != nil); + + self = [super init]; + if (self) { + if (!origin) { + if (pError) { + NSDictionary *info = @{ NSLocalizedDescriptionKey: @"cannot initialize SPDYSession without origin" }; + *pError = [[NSError alloc] initWithDomain:SPDYSessionErrorDomain + code:SPDYSessionInternalError + userInfo:info]; + } + return nil; + } + + SPDYSocket *socket = [[SPDYSocket alloc] initWithDelegate:self]; + bool connecting = [socket connectToHost:origin.host + onPort:origin.port + withTimeout:(NSTimeInterval)60.0 + error:pError]; + + if (connecting) { + _socket = socket; + _origin = origin; + SPDY_INFO(@"session connecting to %@", _origin); + + if ([_origin.scheme isEqualToString:@"https"]) { + SPDY_DEBUG(@"session using TLS"); + [_socket secureWithTLS:@{ /* use Apple default TLS settings */ }]; + } + + _frameDecoder = [[SPDYFrameDecoder alloc] initWithDelegate:self]; + _frameEncoder = [[SPDYFrameEncoder alloc] initWithDelegate:self + headerCompressionLevel:configuration.headerCompressionLevel]; + _activeStreams = [[SPDYStreamManager alloc] init]; + _inactiveStreams = [[SPDYStreamManager alloc] init]; + _inputBuffer = [[NSMutableData alloc] initWithCapacity:INITIAL_INPUT_BUFFER_SIZE]; + + _lastGoodStreamId = 0; + _nextStreamId = 1; + _bufferReadIndex = 0; + _bufferWriteIndex = 0; + + _initialSendWindowSize = DEFAULT_WINDOW_SIZE; + _initialReceiveWindowSize = configuration.streamReceiveWindow; + _localMaxConcurrentStreams = LOCAL_MAX_CONCURRENT_STREAMS; + _remoteMaxConcurrentStreams = REMOTE_MAX_CONCURRENT_STREAMS; + _enableSettingsMinorVersion = configuration.enableSettingsMinorVersion; + + SPDYSettings *settings = [SPDYSettingsStore settingsForOrigin:_origin]; + if (settings != NULL) { + if (settings[SPDY_SETTINGS_MAX_CONCURRENT_STREAMS].set) { + _remoteMaxConcurrentStreams = (NSUInteger)MAX(settings[SPDY_SETTINGS_MAX_CONCURRENT_STREAMS].value, 0); + } + + if (settings[SPDY_SETTINGS_INITIAL_WINDOW_SIZE].set) { + _initialSendWindowSize = (NSUInteger)MAX(settings[SPDY_SETTINGS_INITIAL_WINDOW_SIZE].value, 0); + } + } + + _sessionSendWindowSize = DEFAULT_WINDOW_SIZE; + _sessionReceiveWindowSize = configuration.sessionReceiveWindow; + _sentGoAwayFrame = NO; + _receivedGoAwayFrame = NO; + + [_socket readDataWithTimeout:(NSTimeInterval)-1 + buffer:_inputBuffer + bufferOffset:_bufferWriteIndex + tag:0]; + + [self _sendServerPersistedSettings:settings]; + [self _sendClientSettings]; + +#if ENABLE_SESSION_FLOW_CONTROL + NSUInteger deltaWindowSize = _sessionReceiveWindowSize - DEFAULT_WINDOW_SIZE; + [self _sendWindowUpdate:deltaWindowSize streamId:kSPDYSessionStreamId]; +#endif + } else { + self = nil; + } + } + return self; +} + +- (void)issueRequest:(SPDYProtocol *)protocol +{ + SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol dataDelegate:self]; + + if (_activeStreams.localCount >= _remoteMaxConcurrentStreams) { + [_inactiveStreams addStream:stream]; + SPDY_INFO(@"max concurrent streams reached, deferring request"); + return; + } + + [self _startStream:stream]; +} + +- (void)_issuePendingRequests +{ + SPDYStream *stream; + + while (_inactiveStreams.localCount > 0 && _activeStreams.localCount < _remoteMaxConcurrentStreams) { + stream = [_inactiveStreams nextPriorityStream]; + [_inactiveStreams removeStreamForProtocol:stream.protocol]; + [self _startStream:stream]; + } +} + +- (void)_startStream:(SPDYStream *)stream +{ + SPDYStreamId streamId = [self nextStreamId]; + [stream startWithStreamId:streamId + sendWindowSize:_initialSendWindowSize + receiveWindowSize:_initialReceiveWindowSize]; + _activeStreams[streamId] = stream; + + if (!stream.hasDataPending) { + [self _sendSynStream:stream streamId:streamId closeLocal:YES]; + stream.localSideClosed = YES; + } else { + [self _sendSynStream:stream streamId:streamId closeLocal:NO]; + [self _sendData:stream]; + } +} + +- (void)cancelRequest:(SPDYProtocol *)protocol +{ + SPDYStream *stream = _activeStreams[protocol]; + if (!stream) { + stream = _inactiveStreams[protocol]; + } + + if (stream) { + [self _sendRstStream:SPDY_STREAM_CANCEL streamId:stream.streamId]; + stream.client = nil; + [_activeStreams removeStreamForProtocol:protocol]; + [_inactiveStreams removeStreamForProtocol:protocol]; + [self _issuePendingRequests]; + } +} + +- (void)dealloc +{ + _socket.delegate = nil; + _frameDecoder.delegate = nil; + _frameEncoder.delegate = nil; + [_socket disconnect]; +} + +- (bool)isOpen +{ + return (!_receivedGoAwayFrame && !_sentGoAwayFrame); +} + +- (void)close +{ + [self _closeWithStatus:SPDY_SESSION_OK]; +} + +- (void)_closeWithStatus:(SPDYSessionStatus)status +{ + if (!_sentGoAwayFrame) { + [self _sendGoAway:status]; + } + for (SPDYStream *stream in _activeStreams) { + [self _sendRstStream:SPDY_STREAM_CANCEL streamId:stream.streamId]; + [stream closeWithStatus:stream.local ? SPDY_STREAM_CANCEL : SPDY_STREAM_INTERNAL_ERROR]; + } + + [_activeStreams removeAllStreams]; + [_socket disconnectAfterWrites]; +} + +#pragma mark SPDYSocketDelegate + +- (bool)socket:(SPDYSocket *)socket securedWithTrust:(SecTrustRef)trust +{ + id evaluator = [SPDYProtocol sharedTLSTrustEvaluator]; + return evaluator == nil || [evaluator evaluateServerTrust:trust forHost:_origin.host]; +} + +- (void)socket:(SPDYSocket *)socket didConnectToHost:(NSString *)host port:(in_port_t)port +{ + SPDY_DEBUG(@"socket connected to %@:%u", host, port); + time(&_lastSocketActivity); +} + +- (void)socket:(SPDYSocket *)socket didReadData:(NSData *)data withTag:(long)tag +{ + SPDY_DEBUG(@"socket read[%li] (%lu)", tag, (unsigned long)data.length); + time(&_lastSocketActivity); + + _bufferWriteIndex += data.length; + NSUInteger readableLength = _bufferWriteIndex - _bufferReadIndex; + NSError *error = nil; + + // Decode as much as possible + uint8_t *bytes = (uint8_t *)_inputBuffer.bytes + _bufferReadIndex; + NSUInteger bytesRead = [_frameDecoder decode:bytes length:readableLength error:&error]; + + // Close session on decoding errors + if (error) { + [self _closeWithStatus:SPDY_SESSION_PROTOCOL_ERROR]; + return; + } + + _bufferReadIndex += bytesRead; + + // If we've successfully decoded all available input, reset the buffer + if (_bufferReadIndex == _bufferWriteIndex) { + _bufferReadIndex = 0; + _bufferWriteIndex = 0; + } + + SPDY_DEBUG(@"socket scheduling read[%li] (%lu:%lu)", (tag + 1), (unsigned long)_bufferReadIndex, (unsigned long)_bufferWriteIndex); + [socket readDataWithTimeout:(NSTimeInterval)-1 + buffer:_inputBuffer + bufferOffset:_bufferWriteIndex + tag:(tag + 1)]; +} + +- (void)socket:(SPDYSocket *)socket didWriteDataWithTag:(long)tag +{ + time(&_lastSocketActivity); +} + +- (void)socket:(SPDYSocket *)socket didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag +{ + time(&_lastSocketActivity); +} + +- (void)socket:(SPDYSocket *)socket willDisconnectWithError:(NSError *)error +{ + SPDY_WARNING(@"session connection error: %@", error); + for (SPDYStream *stream in _activeStreams) { + [stream closeWithError:error]; + } + [_activeStreams removeAllStreams]; +} + +- (void)socketDidDisconnect:(SPDYSocket *)socket +{ + SPDY_INFO(@"session connection closed"); + [SPDYSessionManager sessionClosed:self]; +} + +#pragma mark SPDYStreamDataDelegate + +- (void)streamDataAvailable:(SPDYStream *)stream +{ + SPDY_DEBUG(@"request body stream data available"); + [self _sendData:stream]; +} + +- (void)streamFinished:(SPDYStream *)stream +{ + SPDY_DEBUG(@"request body stream finished"); + [self _sendData:stream]; +} + +#pragma mark SPDYFrameEncoderDelegate + +- (void)didEncodeData:(NSData *)data frameEncoder:(SPDYFrameEncoder *)encoder +{ + [_socket writeData:data withTimeout:(NSTimeInterval)-1 tag:0]; +} + +#pragma mark SPDYFrameDecoderDelegate + +- (void)didReadDataFrame:(SPDYDataFrame *)dataFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder +{ + /* + * SPDY Data frame processing requirements: + * + * If an endpoint receives a data frame for a Stream-ID which is not open + * and the endpoint has not sent a GOAWAY frame, it must issue a stream error + * with the error code INVALID_STREAM for the Stream-ID. + * + * If an endpoint which created the stream receives a data frame before receiving + * a SYN_REPLY on that stream, it is a protocol error, and the recipient must + * issue a stream error with the status code PROTOCOL_ERROR for the Stream-ID. + * + * If an endpoint receives multiple data frames for invalid Stream-IDs, + * it may close the session. + * + * If an endpoint refuses a stream it must ignore any data frames for that stream. + * + * If an endpoint receives a data frame after the stream is half-closed from the + * sender, it must send a RST_STREAM frame with the status STREAM_ALREADY_CLOSED. + * + * If an endpoint receives a data frame after the stream is closed, it must send + * a RST_STREAM frame with the status PROTOCOL_ERROR. + */ + + SPDYStreamId streamId = dataFrame.streamId; + SPDYStream *stream = _activeStreams[streamId]; + SPDY_DEBUG(@"received DATA.%u%@ (%lu)", streamId, dataFrame.last ? @"!" : @"", (unsigned long)dataFrame.data.length); + +#if ENABLE_SESSION_FLOW_CONTROL + // Check if session flow control is violated + if (_sessionReceiveWindowSize < dataFrame.data.length) { + [self _closeWithStatus:SPDY_SESSION_PROTOCOL_ERROR]; + return; + } + + // Update session receive window size + _sessionReceiveWindowSize -= dataFrame.data.length; + + // Send a WINDOW_UPDATE frame if less than half the session window size remains + if (_sessionReceiveWindowSize <= _initialReceiveWindowSize / 2) { + NSUInteger deltaWindowSize = _initialReceiveWindowSize - _sessionReceiveWindowSize; + [self _sendWindowUpdate:deltaWindowSize streamId:kSPDYSessionStreamId]; + _sessionReceiveWindowSize = _initialReceiveWindowSize; + } +#endif + + // Check if we received a data frame for a valid Stream-ID + if (!stream) { + if (streamId < _lastGoodStreamId) { + [self _sendRstStream:SPDY_STREAM_PROTOCOL_ERROR streamId:streamId]; + } else if (!_sentGoAwayFrame) { + [self _sendRstStream:SPDY_STREAM_INVALID_STREAM streamId:streamId]; + } + return; + } + + // Check if we received a data frame for a stream which is half-closed + if (stream.remoteSideClosed) { + [self _sendRstStream:SPDY_STREAM_STREAM_ALREADY_CLOSED streamId:streamId]; + return; + } + + // Check if we received a data frame before receiving a SYN_REPLY + if (stream.local && !stream.receivedReply) { + SPDY_WARNING(@"received data before SYN_REPLY"); + [self _sendRstStream:SPDY_STREAM_PROTOCOL_ERROR streamId:streamId]; + return; + } + + /* + * SPDY Data frame flow control processing requirements: + * + * Recipient should not send a WINDOW_UPDATE frame as it consumes the last data frame. + */ + + // Window size can become negative if we sent a SETTINGS frame that reduces the + // size of the transfer window after the peer has written data frames. + // The value is bounded by the length that SETTINGS frame decrease the window. + // This difference is stored for the session when writing the SETTINGS frame + // and is cleared once we send a WINDOW_UPDATE frame. + // Note this can't currently happen in this implementation. + if (stream.receiveWindowSize - dataFrame.data.length < stream.receiveWindowSizeLowerBound) { + [self _sendRstStream:SPDY_STREAM_FLOW_CONTROL_ERROR streamId:streamId]; + return; + } + + // Window size became negative due to sender writing frame before receiving SETTINGS + // Send data frames upstream in initialReceiveWindowSize chunks + if (dataFrame.data.length > _initialReceiveWindowSize) { + NSUInteger dataOffset = 0; + while (dataFrame.data.length - dataOffset > _initialReceiveWindowSize) { + SPDYDataFrame *partialDataFrame = [[SPDYDataFrame alloc] init]; + partialDataFrame.streamId = streamId; + partialDataFrame.last = NO; + + uint8_t *offsetBytes = ((uint8_t *)dataFrame.data.bytes + dataOffset); + NSUInteger chunkLength = MIN(_initialReceiveWindowSize, dataFrame.data.length - dataOffset); + partialDataFrame.data = [[NSData alloc] initWithBytesNoCopy:offsetBytes length:chunkLength freeWhenDone:NO]; + dataOffset += chunkLength; + + if (dataFrame.data.length - dataOffset <= _initialReceiveWindowSize) { + partialDataFrame.last = dataFrame.last; + } + + [self didReadDataFrame:partialDataFrame frameDecoder:frameDecoder]; + } + return; + } + + // Update receive window size + stream.receiveWindowSize -= dataFrame.data.length; + + // Send a WINDOW_UPDATE frame if less than half the window size remains + if (stream.receiveWindowSize <= _initialReceiveWindowSize / 2 && !dataFrame.last) { + // stream.receiveWindowSizeLowerBound = 0; + [self _sendWindowUpdate:_initialReceiveWindowSize - stream.receiveWindowSize streamId:streamId]; + stream.receiveWindowSize = _initialReceiveWindowSize; + } + + [stream didLoadData:dataFrame.data]; + + stream.remoteSideClosed = dataFrame.last; + if (stream.closed) { + [_activeStreams removeStreamWithStreamId:streamId]; + [self _issuePendingRequests]; + } +} + +- (void)didReadSynStreamFrame:(SPDYSynStreamFrame *)synStreamFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder +{ + /* + * SPDY SYN_STREAM frame processing requirements: + * + * If an endpoint receives a SYN_STREAM with a Stream-ID that is less than + * any previously received SYN_STREAM, it must issue a session error with + * the status PROTOCOL_ERROR. + * + * If an endpoint receives multiple SYN_STREAM frames with the same active + * Stream-ID, it must issue a stream error with the status code PROTOCOL_ERROR. + * + * The recipient can reject a stream by sending a stream error with the + * status code REFUSED_STREAM. + */ + + SPDYStreamId streamId = synStreamFrame.streamId; + SPDY_DEBUG(@"received SYN_STREAM.%u", streamId); + + // Stream-IDs must be monotonically increasing + if (streamId <= _lastGoodStreamId) { + [self _closeWithStatus:SPDY_SESSION_PROTOCOL_ERROR]; + return; + } + + if (_receivedGoAwayFrame || _activeStreams.remoteCount >= _localMaxConcurrentStreams) { + [self _sendRstStream:SPDY_STREAM_REFUSED_STREAM streamId:streamId]; + return; + } + + SPDYStream *stream = [[SPDYStream alloc] init]; + stream.priority = synStreamFrame.priority; + stream.remoteSideClosed = synStreamFrame.last; + stream.sendWindowSize = _initialSendWindowSize; + stream.receiveWindowSize = _initialReceiveWindowSize; + + _lastGoodStreamId = streamId; + _activeStreams[streamId] = stream; +} + +- (void)didReadSynReplyFrame:(SPDYSynReplyFrame *)synReplyFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder +{ + /* + * SPDY SYN_REPLY frame processing requirements: + * + * If an endpoint receives multiple SYN_REPLY frames for the same active Stream-ID + * it must issue a stream error with the status code STREAM_IN_USE. + */ + + SPDYStreamId streamId = synReplyFrame.streamId; + SPDYStream *stream = _activeStreams[streamId]; + SPDY_DEBUG(@"received SYN_REPLY.%u%@ (%@)", streamId, synReplyFrame.last ? @"!" : @"", synReplyFrame.headers[@":status"] ?: @"-"); + + // Check if this is a reply for an active stream + if (!stream) { + [self _sendRstStream:SPDY_STREAM_INVALID_STREAM streamId:streamId]; + return; + } + + // Check if we have received multiple frames for the same Stream-ID + if (stream.receivedReply) { + [self _sendRstStream:SPDY_STREAM_STREAM_IN_USE streamId:streamId]; + return; + } + + [stream didReceiveResponse:synReplyFrame.headers]; + + stream.remoteSideClosed = synReplyFrame.last; + + + if (stream.closed) { + [_activeStreams removeStreamWithStreamId:streamId]; + [self _issuePendingRequests]; + } +} + +- (void)didReadRstStreamFrame:(SPDYRstStreamFrame *)rstStreamFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder +{ + /* + * SPDY RST_STREAM frame processing requirements: + * + * After receiving a RST_STREAM on a stream, the receiver must not send + * additional frames on that stream. + * + * An endpoint must not send a RST_STREAM in response to a RST_STREAM. + */ + + SPDYStreamId streamId = rstStreamFrame.streamId; + SPDYStream *stream = _activeStreams[streamId]; + SPDY_DEBUG(@"received RST_STREAM.%u", streamId); + + if (stream) { + [stream closeWithStatus:rstStreamFrame.statusCode]; + [_activeStreams removeStreamWithStreamId:streamId]; + [self _issuePendingRequests]; + } +} + +- (void)didReadSettingsFrame:(SPDYSettingsFrame *)settingsFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder +{ + /* + * SPDY SETTINGS frame processing requirements: + * + * When a client connects to a server, and the server persists settings + * within the client, the client should return the persisted settings on + * future connections to the same origin and IP address and TCP port (the + * "origin" is the set of scheme, host, and port from the URI). + */ + + SPDY_DEBUG(@"received SETTINGS"); + + SPDYSettings *settings = settingsFrame.settings; + + if (settingsFrame.clearSettings) { + [SPDYSettingsStore clearSettingsForOrigin:nil]; + } + + bool persistSettings = NO; + + for (SPDYSettingsId i = _SPDY_SETTINGS_RANGE_START; !persistSettings && i < _SPDY_SETTINGS_RANGE_END; i++) { + // Check if any settings need to be persisted before dispatching + if (settings[i].set && settings[i].flags == SPDY_SETTINGS_FLAG_PERSIST_VALUE) { + [SPDYSettingsStore persistSettings:settings forOrigin:_origin]; + persistSettings = YES; + } + } + + if (settings[SPDY_SETTINGS_MAX_CONCURRENT_STREAMS].set) { + _remoteMaxConcurrentStreams = (NSUInteger)MAX(settings[SPDY_SETTINGS_MAX_CONCURRENT_STREAMS].value, 0); + } + + if (settings[SPDY_SETTINGS_INITIAL_WINDOW_SIZE].set) { + NSUInteger previousWindowSize = _initialSendWindowSize; + _initialSendWindowSize = (NSUInteger)MAX(settings[SPDY_SETTINGS_INITIAL_WINDOW_SIZE].value, 0); + NSUInteger deltaWindowSize = _initialSendWindowSize - previousWindowSize; + + for (SPDYStream *stream in _activeStreams) { + if (!stream.localSideClosed) { + stream.sendWindowSize = stream.sendWindowSize + deltaWindowSize; + [self _sendData:stream]; + } + } + } + + [self _issuePendingRequests]; +} + +- (void)didReadPingFrame:(SPDYPingFrame *)pingFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder +{ + /* + * SPDY PING frame processing requirements: + * + * Receivers of a PING frame should send an identical frame to the sender + * as soon as possible. + * + * Receivers of a PING frame must ignore frames that it did not initiate + */ + + SPDY_DEBUG(@"received PING"); + + if (!(pingFrame.id & 1)) { + [self _sendPingResponse:pingFrame]; + } +} + +- (void)didReadGoAwayFrame:(SPDYGoAwayFrame *)goAwayFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder +{ + SPDY_DEBUG(@"received GOAWAY (%u)", goAwayFrame.statusCode); + + _receivedGoAwayFrame = YES; + + if (_activeStreams.count == 0) { + [_socket disconnect]; + } +} + +- (void)didReadHeadersFrame:(SPDYHeadersFrame *)headersFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder +{ + SPDYStreamId streamId = headersFrame.streamId; + SPDYStream *stream = _activeStreams[streamId]; + SPDY_DEBUG(@"received HEADERS.%u", streamId); + + if (!stream || stream.remoteSideClosed) { + [self _sendRstStream:SPDY_STREAM_INVALID_STREAM streamId:streamId]; + return; + } + + stream.remoteSideClosed = headersFrame.last; + if (stream.closed) { + [_activeStreams removeStreamWithStreamId:streamId]; + [self _issuePendingRequests]; + } +} + +- (void)didReadWindowUpdateFrame:(SPDYWindowUpdateFrame *)windowUpdateFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder +{ + /* + * SPDY WINDOW_UPDATE frame processing requirements: + * + * Receivers of a WINDOW_UPDATE that cause the window size to exceed 2^31 + * must send a RST_STREAM with the status code FLOW_CONTROL_ERROR. + * + * Sender should ignore all WINDOW_UPDATE frames associated with a stream + * after sending the last frame for the stream. + */ + + SPDYStreamId streamId = windowUpdateFrame.streamId; + SPDY_DEBUG(@"received WINDOW_UPDATE.%u (+%lu)", streamId, (unsigned long)windowUpdateFrame.deltaWindowSize); + +#if ENABLE_SESSION_FLOW_CONTROL + if (streamId == kSPDYSessionStreamId) { + // Check for numerical overflow + if (_sessionSendWindowSize > INT32_MAX - windowUpdateFrame.deltaWindowSize) { + [self _closeWithStatus:SPDY_SESSION_PROTOCOL_ERROR]; + return; + } + + _sessionSendWindowSize += windowUpdateFrame.deltaWindowSize; + for (SPDYStream *stream in _activeStreams) { + [self _sendData:stream]; + if (_sessionSendWindowSize == 0) break; + } + + return; + } +#endif + + // Ignore frames for non-existent or half-closed streams + SPDYStream *stream = _activeStreams[streamId]; + if (!stream || stream.localSideClosed) { + return; + } + + // Check for numerical overflow + if (stream.sendWindowSize > INT32_MAX - windowUpdateFrame.deltaWindowSize) { + [self _sendRstStream:SPDY_STREAM_FLOW_CONTROL_ERROR streamId:streamId]; + return; + } + + stream.sendWindowSize += windowUpdateFrame.deltaWindowSize; + [self _sendData:stream]; +} + +#pragma mark private methods + +- (SPDYStreamId)nextStreamId +{ + SPDYStreamId streamId = _nextStreamId; + _nextStreamId += 2; + return streamId; +} + +- (void)_sendServerPersistedSettings:(SPDYSettings *)persistedSettings +{ + if (persistedSettings != NULL) { + SPDYSettingsFrame *settingsFrame = [[SPDYSettingsFrame alloc] init]; + SPDY_SETTINGS_ITERATOR(i) { + settingsFrame.settings[i] = persistedSettings[i]; + } + + [_frameEncoder encodeSettingsFrame:settingsFrame]; + SPDY_DEBUG(@"sent server SETTINGS"); + } +} + +- (void)_sendClientSettings +{ + SPDYSettingsFrame *settingsFrame = [[SPDYSettingsFrame alloc] init]; + if (_enableSettingsMinorVersion) { + settingsFrame.settings[SPDY_SETTINGS_MINOR_VERSION].set = YES; + settingsFrame.settings[SPDY_SETTINGS_MINOR_VERSION].flags = 0; + settingsFrame.settings[SPDY_SETTINGS_MINOR_VERSION].value = ENABLE_SESSION_FLOW_CONTROL; + } + settingsFrame.settings[SPDY_SETTINGS_INITIAL_WINDOW_SIZE].set = YES; + settingsFrame.settings[SPDY_SETTINGS_INITIAL_WINDOW_SIZE].flags = 0; + settingsFrame.settings[SPDY_SETTINGS_INITIAL_WINDOW_SIZE].value = (int32_t)_initialReceiveWindowSize; + + [_frameEncoder encodeSettingsFrame:settingsFrame]; + SPDY_DEBUG(@"sent client SETTINGS"); +} + +- (void)_sendSynStream:(SPDYStream *)stream streamId:(SPDYStreamId)streamId closeLocal:(bool)close +{ + SPDYSynStreamFrame *synStreamFrame = [[SPDYSynStreamFrame alloc] init]; + synStreamFrame.streamId = streamId; + synStreamFrame.priority = stream.priority; + synStreamFrame.unidirectional = NO; + synStreamFrame.slot = 0; + synStreamFrame.associatedToStreamId = 0; + synStreamFrame.last = close; + synStreamFrame.headers = stream.protocol.request.allSPDYHeaderFields; + + [_frameEncoder encodeSynStreamFrame:synStreamFrame]; + SPDY_DEBUG(@"sent SYN_STREAM.%u%@",streamId, synStreamFrame.last ? @"!" : @""); +} + +- (void)_sendData:(SPDYStream *)stream +{ + SPDYStreamId streamId = stream.streamId; + +#if ENABLE_SESSION_FLOW_CONTROL + NSUInteger sendWindowSize = MIN(_sessionSendWindowSize, stream.sendWindowSize); +#else + NSUInteger sendWindowSize = stream.sendWindowSize; +#endif + + while (!stream.localSideClosed && stream.hasDataAvailable && sendWindowSize > 0) { + NSError *error; + NSData *data = [stream readData:sendWindowSize error:&error]; + + if (data) { + SPDYDataFrame *dataFrame = [[SPDYDataFrame alloc] init]; + dataFrame.streamId = streamId; + dataFrame.data = data; + dataFrame.last = !stream.hasDataPending; + [_frameEncoder encodeDataFrame:dataFrame]; + SPDY_DEBUG(@"sent DATA.%u%@ (%lu)", streamId, dataFrame.last ? @"!" : @"", (unsigned long)dataFrame.data.length); + + NSUInteger bytesSent = data.length; + sendWindowSize -= bytesSent; + +#if !ENABLE_SESSION_FLOW_CONTROL + _sessionSendWindowSize -= bytesSent; +#endif + + stream.sendWindowSize -= bytesSent; + stream.localSideClosed = dataFrame.last; + } else { + if (error) { + [self _sendRstStream:SPDY_STREAM_CANCEL streamId:streamId]; + [stream closeWithError:error]; + [_activeStreams removeStreamWithStreamId:streamId]; + [self _issuePendingRequests]; + } + + // -[SPDYStream hasDataAvailable] may return true if we need to perform + // a read on a stream to determine if data is actually available. This + // mirrors Apple's API with [NSStream -hasBytesAvailable]. The break here + // should technically be unnecessary, but it doesn't hurt. + break; + } + } + + if (!stream.hasDataPending && !stream.localSideClosed) { + SPDYDataFrame *dataFrame = [[SPDYDataFrame alloc] init]; + dataFrame.streamId = streamId; + dataFrame.last = YES; + [_frameEncoder encodeDataFrame:dataFrame]; + SPDY_DEBUG(@"sent DATA.%u%@ (%lu)", streamId, dataFrame.last ? @"!" : @"", (unsigned long)dataFrame.data.length); + + stream.localSideClosed = YES; + } + + if (stream.closed) { + [_activeStreams removeStreamWithStreamId:streamId]; + [self _issuePendingRequests]; + } +} + +- (void)_sendWindowUpdate:(NSUInteger)deltaWindowSize streamId:(SPDYStreamId)streamId +{ + SPDYWindowUpdateFrame *windowUpdateFrame = [[SPDYWindowUpdateFrame alloc] init]; + windowUpdateFrame.streamId = streamId; + windowUpdateFrame.deltaWindowSize = deltaWindowSize; + [_frameEncoder encodeWindowUpdateFrame:windowUpdateFrame]; + SPDY_DEBUG(@"sent WINDOW_UPDATE.%u (+%lu)", streamId, (unsigned long)deltaWindowSize); +} + +- (void)_sendPingResponse:(SPDYPingFrame *)pingFrame +{ + [_frameEncoder encodePingFrame:pingFrame]; + SPDY_DEBUG(@"sent PING response"); +} + +- (void)_sendRstStream:(SPDYStreamStatus)status streamId:(SPDYStreamId)streamId +{ + SPDYRstStreamFrame *rstStreamFrame = [[SPDYRstStreamFrame alloc] init]; + rstStreamFrame.streamId = streamId; + rstStreamFrame.statusCode = status; + [_frameEncoder encodeRstStreamFrame:rstStreamFrame]; + SPDY_DEBUG(@"sent RST_STREAM.%u", streamId); +} + +- (void)_sendGoAway:(SPDYSessionStatus)status +{ + SPDYGoAwayFrame *goAwayFrame = [[SPDYGoAwayFrame alloc] init]; + goAwayFrame.lastGoodStreamId = _lastGoodStreamId; + goAwayFrame.statusCode = status; + [_frameEncoder encodeGoAwayFrame:goAwayFrame]; + SPDY_DEBUG(@"sent GO_AWAY"); + _sentGoAwayFrame = YES; +} + +@end diff --git a/SPDY/SPDYSessionManager.h b/SPDY/SPDYSessionManager.h new file mode 100644 index 0000000..743f387 --- /dev/null +++ b/SPDY/SPDYSessionManager.h @@ -0,0 +1,21 @@ +// +// SPDYSessionManager.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import + +@class SPDYConfiguration; +@class SPDYSession; + +@interface SPDYSessionManager : NSObject ++ (void)setConfiguration:(SPDYConfiguration *)configuration; ++ (SPDYSession *)sessionForURL:(NSURL *)url error:(NSError **)pError; ++ (void)sessionClosed:(SPDYSession *)session; +@end diff --git a/SPDY/SPDYSessionManager.m b/SPDY/SPDYSessionManager.m new file mode 100644 index 0000000..7f1f8f9 --- /dev/null +++ b/SPDY/SPDYSessionManager.m @@ -0,0 +1,76 @@ +// +// SPDYSessionManager.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "SPDYOrigin.h" +#import "SPDYSessionManager.h" +#import "SPDYProtocol.h" +#import "SPDYSession.h" + +@interface SPDYSessionManager () ++ (NSMutableDictionary *)activeSessions; +@end + +static NSString *const SPDYSessionManagerKey = @"com.twitter.SPDYSessionManager"; +static SPDYConfiguration *currentConfiguration; + +@implementation SPDYSessionManager + ++ (void)initialize +{ + currentConfiguration = [SPDYConfiguration defaultConfiguration]; +} + ++ (NSMutableDictionary *)activeSessions +{ + NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; + NSMutableDictionary *activeSessions = threadDictionary[SPDYSessionManagerKey]; + if (!activeSessions) { + activeSessions = [NSMutableDictionary new]; + threadDictionary[SPDYSessionManagerKey] = activeSessions; + } + return activeSessions; +} + ++ (void)setConfiguration:(SPDYConfiguration *)configuration +{ + currentConfiguration = [configuration copy]; +} + ++ (SPDYSession *)sessionForURL:(NSURL *)url error:(NSError **)pError +{ + SPDYOrigin *origin = [[SPDYOrigin alloc] initWithURL:url error:pError]; + NSMutableDictionary *activeSessions = [SPDYSessionManager activeSessions]; + SPDYSession *session = activeSessions[origin]; + if (!session || !session.isOpen) { + session = [[SPDYSession alloc] initWithOrigin:origin + configuration:currentConfiguration + error:pError]; + if (session) { + activeSessions[origin] = session; + } + } + return session; +} + ++ (void)sessionClosed:(SPDYSession *)session +{ + SPDYOrigin *origin = session.origin; + NSMutableDictionary *activeSessions = [SPDYSessionManager activeSessions]; + if (activeSessions[origin] == session) { + [activeSessions removeObjectForKey:origin]; + } +} + +@end diff --git a/SPDY/SPDYSettingsStore.h b/SPDY/SPDYSettingsStore.h new file mode 100644 index 0000000..57255cf --- /dev/null +++ b/SPDY/SPDYSettingsStore.h @@ -0,0 +1,21 @@ +// +// SPDYSettingsStore.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import +#import "SPDYDefinitions.h" + +@class SPDYOrigin; + +@interface SPDYSettingsStore : NSObject ++ (SPDYSettings *)settingsForOrigin:(SPDYOrigin *)origin; ++ (void)persistSettings:(SPDYSettings *)settings forOrigin:(SPDYOrigin *)origin; ++ (void)clearSettingsForOrigin:(SPDYOrigin *)origin; +@end diff --git a/SPDY/SPDYSettingsStore.m b/SPDY/SPDYSettingsStore.m new file mode 100644 index 0000000..a752f50 --- /dev/null +++ b/SPDY/SPDYSettingsStore.m @@ -0,0 +1,115 @@ +// +// SPDYSettingsStore.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "SPDYSettingsStore.h" +#import "SPDYOrigin.h" + +// Convenience wrapper for SPDYSettings array +@interface SPDYSettingsObj : NSObject +@property (nonatomic, readonly) SPDYSettings *settings; +@end + +@implementation SPDYSettingsObj +{ + SPDYSettings _settings[SPDY_SETTINGS_LENGTH]; +} + +- (id)init +{ + self = [super init]; + if (self) { + SPDY_SETTINGS_ITERATOR(i) { + _settings[i].set = NO; + } + } + return self; +} + +- (SPDYSettings *)settings +{ + return _settings; +} + +@end + +@interface SPDYSettingsStore () ++ (NSMutableDictionary *)_sharedStore; +@end + +@implementation SPDYSettingsStore + ++ (SPDYSettings *)settingsForOrigin:(SPDYOrigin *)origin +{ + NSMutableDictionary *sharedStore = [SPDYSettingsStore _sharedStore]; + SPDYSettingsObj *storedSettingsObj = sharedStore[origin]; + + if (!storedSettingsObj) { + return NULL; + } + + return storedSettingsObj.settings; +} + ++ (void)persistSettings:(SPDYSettings *)settings forOrigin:(SPDYOrigin *)origin +{ + NSMutableDictionary *sharedStore = [SPDYSettingsStore _sharedStore]; + SPDYSettingsObj *storedSettingsObj = sharedStore[origin]; + + if (!storedSettingsObj) { + storedSettingsObj = [[SPDYSettingsObj alloc] init]; + sharedStore[origin] = storedSettingsObj; + } + + SPDYSettings *storedSettings = storedSettingsObj.settings; + + SPDY_SETTINGS_ITERATOR(i) { + if (settings[i].set && settings[i].flags == SPDY_SETTINGS_FLAG_PERSIST_VALUE) { + storedSettings[i].set = YES; + storedSettings[i].flags = SPDY_SETTINGS_FLAG_PERSISTED; + storedSettings[i].value = settings[i].value; + } + } +} + + ++ (void)clearSettingsForOrigin:(SPDYOrigin *)origin +{ + NSMutableDictionary *sharedStore = [SPDYSettingsStore _sharedStore]; + SPDYSettingsObj *storedSettingsObj = sharedStore[origin]; + + if (!storedSettingsObj) { + return; + } + + SPDYSettings *storedSettings = storedSettingsObj.settings; + + SPDY_SETTINGS_ITERATOR(i) { + storedSettings[i].set = NO; + } +} + +#pragma mark private methods + ++ (NSMutableDictionary *)_sharedStore +{ + static dispatch_once_t pred; + static NSMutableDictionary *sharedStore; + dispatch_once(&pred, ^{ + sharedStore = [[NSMutableDictionary alloc] init]; + }); + return sharedStore; +} + +@end diff --git a/SPDY/SPDYSocket.h b/SPDY/SPDYSocket.h new file mode 100644 index 0000000..c953e9b --- /dev/null +++ b/SPDY/SPDYSocket.h @@ -0,0 +1,354 @@ +// +// SPDYSocket.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Substantially based on the CocoaAsyncSocket library, originally +// created by Dustin Voss, and currently maintained at +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#import +#import "SPDYError.h" + +@class SPDYSocket; +@class SPDYSocketReadOp; +@class SPDYSocketWriteOp; + +extern NSString *const SPDYSocketException; + +#pragma mark SPDYSocketDelegate + +@protocol SPDYSocketDelegate +@optional + +/** + Called when a socket encounters an error and will be closing. + + You may call [SPDYSocket unreadData] during this callback to retrieve + remaining data off the socket. This delegate method may be called before + socket:didAcceptNewSocket: or onSocket:didConnectToHost:. +*/ +- (void)socket:(SPDYSocket *)socket willDisconnectWithError:(NSError *)error; + +/** + Called when a socket disconnects with or without error. + + The SPDYSocket may be safely released during this callback. If you call + [SPDYSocket disconnect], and the socket wasn't already disconnected, this + delegate method will be called before the disconnect method returns. +*/ +- (void)socketDidDisconnect:(SPDYSocket *)socket; + +/** + Called when a socket accepts a connection. + + Another SPDYSocket is spawned to handle it. The new socket will have + the same delegate and will call socket:didConnectToHost:port:. +*/ +- (void)socket:(SPDYSocket *)socket didAcceptNewSocket:(SPDYSocket *)newSocket; + +/** + Called when a new socket is spawned to handle a connection. + + This method should return the run loop on which the new socket and its + delegate should operate. If omitted, [NSRunLoop currentRunLoop] is used. +*/ +- (NSRunLoop *)socket:(SPDYSocket *)socket wantsRunLoopForNewSocket:(SPDYSocket *)newSocket; + +/** + Called when a socket is about to connect. + + If [SPDYSocket connectToHost:onPort:error:] was called, the delegate will be + able to access and configure the CFReadStream and CFWriteStream as desired + prior to connection. + + If [SPDYSocket connectToAddress:error:] was called, the delegate will be able + to access and configure the CFSocket and CFSocketNativeHandle (BSD socket) as + desired prior to connection. You will be able to access and configure the + CFReadStream and CFWriteStream during socket:didConnectToHost:port:. + + @return YES to continue, NO to abort resulting in a SPDYSocketConnectCanceled +*/ +- (bool)socketWillConnect:(SPDYSocket *)socket; + +/** + Called when a socket connects and is ready for reading and writing. + + @param host IP address of the connected host +*/ +- (void)socket:(SPDYSocket *)socket didConnectToHost:(NSString *)host port:(in_port_t)port; + +/** + Called when a socket has completed reading the requested data into memory. +*/ +- (void)socket:(SPDYSocket *)socket didReadData:(NSData *)data withTag:(long)tag; + +/** + Called when a socket has read in data, but has not yet completed the read. + + This would occur if using readToData: or readToLength: methods. +*/ +- (void)socket:(SPDYSocket *)socket didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + Called when a socket has completed writing the requested data. +*/ +- (void)socket:(SPDYSocket *)socket didWriteDataWithTag:(long)tag; + +/** + Called when a socket has written data, but has not yet completed the write. +*/ +- (void)socket:(SPDYSocket *)socket didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; + +/** + Called if a read operation has reached its timeout without completing. + + @param elapsed total elapsed time since the read began + @param length number of bytes that have been read so far + @return a positive value to optionally extend the read's timeout +*/ +- (NSTimeInterval)socket:(SPDYSocket *)socket + willTimeoutReadWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + Called if a write operation has reached its timeout without completing. + + @param elapsed total elapsed time since the write began + @param length number of bytes that have been write so far + @return a positive value to optionally extend the write's timeout +*/ +- (NSTimeInterval)socket:(SPDYSocket *)socket + willTimeoutWriteWithTag:(long)tag + elapsed:(NSTimeInterval)elapsed + bytesDone:(NSUInteger)length; + +/** + Called when a socket has successfully completed SSL/TLS negotiation. + + If the delegate does not implement this method, use of the newly opened + TLS channel will always proceed as if this method had returned YES. + + @param trust the X.509 trust object created to evaluate the TLS channel + @return YES to continue, NO to close the connection with the error + SPDYSocketTLSVerificationFailed +*/ +- (bool)socket:(SPDYSocket *)socket securedWithTrust:(SecTrustRef)trust; + +@end + +#pragma mark SPDYSocket + +@interface SPDYSocket : NSObject +@property (nonatomic, weak) id delegate; + +- (id)initWithDelegate:(id)delegate; +- (CFSocketRef)cfSocket; +- (CFReadStreamRef)cfReadStream; +- (CFWriteStreamRef)cfWriteStream; + +/** + Connects to the given host and port. +*/ +- (bool)connectToHost:(NSString *)hostname onPort:(in_port_t)port error:(NSError **)pError; + +/** + Connects to the given host and port. + + @param timeout use a negative value for no connection timeout +**/ +- (bool)connectToHost:(NSString *)hostname + onPort:(in_port_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)pError; + +/** + Disconnects immediately; any pending reads or writes are dropped. +*/ +- (void)disconnect; + +/** + Disconnects after all pending reads have completed. +*/ +- (void)disconnectAfterReads; + +/** + Disconnects after all pending writes have completed. +*/ +- (void)disconnectAfterWrites; + +/** + Disconnects after all pending reads and writes have completed. +*/ +- (void)disconnectAfterReadsAndWrites; + +/** + @return YES when the socket streams are open and connected +*/ +- (bool)connected; + +/** + @return the IP address of the host to which the socket is connected +*/ +- (NSString *)connectedHost; + +/** + @return the port to which the socket is connected +*/ +- (in_port_t)connectedPort; + +/** + @return YES if the socket is IPv4 +*/ +- (bool)isIPv4; + +/** + @return YES if the socket is IPv6 +*/ +- (bool)isIPv6; + +/** + Asynchronously read the first available bytes on the socket. + + When the read is complete the socket:didReadData:withTag: delegate method + will be called. + + @param timeout use a negative value for no timeout + @param tag an arbitrary tag to associate with the delegate callback +*/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + Asynchronously read the first available bytes on the socket. + + When the read is complete the socket:didReadData:withTag: delegate method + will be called, referencing new bytes written to the specified buffer. + + @param timeout use a negative value for no timeout + @param buffer the buffer to use for reading + @param offset the index to write to in the buffer + @param tag an arbitrary tag to associate with the delegate callback +*/ + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + Asynchronously read the first available bytes on the socket. + + When the read is complete the socket:didReadData:withTag: delegate method + will be called, referencing new bytes written to the specified buffer. + + @param timeout use a negative value for no timeout + @param buffer the buffer to use for reading + @param offset the index to write to in the buffer + @param maxLength the maximum number of bytes to read with this operation + @param tag an arbitrary tag to associate with the delegate callback +*/ +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag; + +/** + Asynchronously read the specified number of bytes off the socket. + + When the read is complete the socket:didReadData:withTag: delegate method + will be called. + + @param length the number of bytes to read before calling the delegate + @param timeout use a negative value for no timeout + @param tag an arbitrary tag to associate with the delegate callback +*/ +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + Asynchronously read the specified number of bytes off the socket. + + When the read is complete the socket:didReadData:withTag: delegate method + will be called, referencing new bytes written to the specified buffer. + + @param length the number of bytes to read before calling the delegate + @param timeout use a negative value for no timeout + @param buffer the buffer to use for reading + @param offset the index to write to in the buffer + @param tag an arbitrary tag to associate with the delegate callback +**/ +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag; + +/** + Asynchronously writes data to the socket. + + When the write is complete the socket:didWriteDataWithTag: delegate method + will be called. + + @param timeout use a negative value for no timeout + @param tag an arbitrary tag to associate with the delegate callback +*/ +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; + +/** + Secures the connection using TLS. + + This method may be called at any time, and the TLS handshake will occur after + all pending reads and writes are finished. + + @param tlsSettings a dictionary of TLS settings to use for the connection + + Possible keys and values for TLS settings can be found in CFSocketStream.h + Some possible keys are: + - kCFStreamSSLLevel + - kCFStreamSSLAllowsExpiredCertificates + - kCFStreamSSLAllowsExpiredRoots + - kCFStreamSSLAllowsAnyRoot + - kCFStreamSSLValidatesCertificateChain + - kCFStreamSSLPeerName + - kCFStreamSSLCertificates + - kCFStreamSSLIsServer + + If you pass nil or an empty dictionary, Apple default settings will be used. +*/ +- (void)secureWithTLS:(NSDictionary *)tlsSettings; + +/** + Reschedule the SPDYSocket on a different runloop. +*/ +- (bool)setRunLoop:(NSRunLoop *)runLoop; + +/** + Configures the runloop modes the SPDYSocket will operate on. + + The default set is limited to NSDefaultRunLoopMode. + + If you'd like your socket to continue operation during other modes, you may want to add modes such as + NSModalPanelRunLoopMode or NSEventTrackingRunLoopMode. Or you may simply want to use NSRunLoopCommonModes. +*/ +- (bool)setRunLoopModes:(NSArray *)runLoopModes; +- (bool)addRunLoopMode:(NSString *)runLoopMode; +- (bool)removeRunLoopMode:(NSString *)runLoopMode; + +/** + @return the current runloop modes the SPDYSocket is scheduled on +*/ +- (NSArray *)runLoopModes; + +/** + Call during socket:willDisconnectWithError: to read any leftover data on the socket. + + @return any remaining data off the socket buffer +*/ +- (NSData *)unreadData; + +@end diff --git a/SPDY/SPDYSocket.m b/SPDY/SPDYSocket.m new file mode 100644 index 0000000..c981752 --- /dev/null +++ b/SPDY/SPDYSocket.m @@ -0,0 +1,1819 @@ +// +// SPDYSocket.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Substantially based on the CocoaAsyncSocket library, originally +// created by Dustin Voss, and currently maintained at +// https://github.com/robbiehanson/CocoaAsyncSocket +// + +#if ! __has_feature(objc_arc) +#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). +#endif + +#import "SPDYSocket.h" +#import "SPDYCommonLogger.h" +#import +#import +#import + +#pragma mark Declarations + +#define READ_QUEUE_CAPACITY 64 // Initial capacity +#define WRITE_QUEUE_CAPACITY 64 // Initial capacity +#define READ_CHUNK_SIZE 65536 // Limit on size of each read pass +#define WRITE_CHUNK_SIZE 2852 // Limit on size of each write pass + +#define DEBUG_THREAD_SAFETY 0 + +#if DEBUG_THREAD_SAFETY +#define CHECK_THREAD_SAFETY() \ +do { \ + if (_runLoop && _runLoop != CFRunLoopGetCurrent()) { \ + [NSException raise:SPDYSocketException \ + format:@"Detected SPDYSocket access from wrong RunLoop"]; \ + } \ +} while (0) +#else +#define CHECK_THREAD_SAFETY() +#endif + +NSString *const SPDYSocketException = @"SPDYSocketException"; + +typedef enum : uint16_t { + kDidStartDelegate = 1 << 0, // If set, disconnection results in delegate call + kDidCompleteOpenForRead = 1 << 1, // If set, open callback has been called for read stream + kDidCompleteOpenForWrite = 1 << 2, // If set, open callback has been called for write stream + kStartingReadTLS = 1 << 3, // If set, we're waiting for TLS negotiation to complete + kStartingWriteTLS = 1 << 4, // If set, we're waiting for TLS negotiation to complete + kForbidReadsWrites = 1 << 5, // If set, no new reads or writes are allowed + kDisconnectAfterReads = 1 << 6, // If set, disconnect after no more reads are queued + kDisconnectAfterWrites = 1 << 7, // If set, disconnect after no more writes are queued + kClosingWithError = 1 << 8, // If set, the socket is being closed due to an error + kDequeueReadScheduled = 1 << 9, // If set, a _dequeueRead operation is already scheduled + kDequeueWriteScheduled = 1 << 10, // If set, a _dequeueWrite operation is already scheduled + kSocketCanAcceptBytes = 1 << 11, // If set, we know socket can accept bytes. If unset, it's unknown. + kSocketHasBytesAvailable = 1 << 12, // If set, we know socket has bytes available. If unset, it's unknown. +} SPDYSocketFlag; + +@interface SPDYSocket () +{ + in_port_t _connectedPort; + NSString *_connectedHost; +} + +// Connecting +- (void)_startConnectTimeout:(NSTimeInterval)timeout; +- (void)_endConnectTimeout; +- (void)_timeoutConnect:(NSTimer *)timer; + +// Stream Implementation +- (bool)_createStreamsToHost:(NSString *)hostname onPort:(in_port_t)port error:(NSError **)pError; +- (bool)_scheduleStreamsOnRunLoop:(NSRunLoop *)runLoop error:(NSError **)pError; +- (bool)_configureStreams:(NSError **)pError; +- (bool)_openStreams:(NSError **)pError; +- (void)_onStreamOpened; +- (bool)_setSocketViaStreams:(NSError **)pError; + +// Disconnect Implementation +- (void)_closeWithError:(NSError *)error; +- (void)_captureUnreadData; +- (void)_emptyQueues; +- (void)_close; + +// Errors +- (NSError *)abortError; +- (NSError *)streamError; +- (NSError *)socketError; +- (NSError *)connectTimeoutError; +- (NSError *)readTimeoutError; +- (NSError *)writeTimeoutError; + +// Diagnostics +- (bool)_fullyDisconnected; +- (void)_setConnectionProperties; + +// Reading +- (void)_read; +- (void)_finishRead; +- (void)_endRead; +- (void)_scheduleRead; +- (void)_dequeueRead; +- (void)_timeoutRead:(NSTimer *)timer; + +// Writing +- (void)_write; +- (void)_finishWrite; +- (void)_endWrite; +- (void)_scheduleWrite; +- (void)_dequeueWrite; +- (void)_timeoutWrite:(NSTimer *)timer; + +// CFRunLoop scheduling +- (void)_addSource:(CFRunLoopSourceRef)source; +- (void)_addTimer:(NSTimer *)timer; +- (void)_removeSource:(CFRunLoopSourceRef)source; +- (void)_removeTimer:(NSTimer *)timer; +- (void)_scheduleDisconnect; +- (void)_unscheduleReadStream; +- (void)_unscheduleWriteStream; + +// TLS +- (void)_tryTLSHandshake; +- (void)_onTLSHandshakeSuccess; + +// Callbacks +- (void)handleCFReadStreamEvent:(CFStreamEventType)type forStream:(CFReadStreamRef)stream; +- (void)handleCFWriteStreamEvent:(CFStreamEventType)type forStream:(CFWriteStreamRef)stream; + +@end + +static void SPDYSocketCFReadStreamCallback(CFReadStreamRef stream, CFStreamEventType type, void *pInfo); +static void SPDYSocketCFWriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType type, void *pInfo); + + +/** + Encompasses the instructions for any given read operation. + + [SPDYSocketDelegate socket:didReadData:withTag:] is called when a read + operation completes. If _fixedLength is set, the delegate will not be called + until exactly that number of bytes is read. If _maxLength is set, the + delegate will be called as soon as 0 < bytes <= maxLength are read. If + neither is set, the delegate will be called as soon as any bytes are read. +*/ +@interface SPDYSocketReadOp : NSObject { + @public + NSMutableData *_buffer; + NSUInteger _bytesRead; + NSUInteger _startOffset; + NSUInteger _maxLength; + NSUInteger _fixedLength; + NSUInteger _originalBufferLength; + NSTimeInterval _timeout; + bool _bufferOwner; + long _tag; +} + +- (id)initWithData:(NSMutableData *)data + startOffset:(NSUInteger)startOffset + maxLength:(NSUInteger)maxLength + timeout:(NSTimeInterval)timeout + fixedLength:(NSUInteger)fixedLength + tag:(long)tag; + +- (NSUInteger)safeReadLength; +@end + +@implementation SPDYSocketReadOp + +- (id)initWithData:(NSMutableData *)data + startOffset:(NSUInteger)startOffset + maxLength:(NSUInteger)maxLength + timeout:(NSTimeInterval)timeout + fixedLength:(NSUInteger)fixedLength + tag:(long)tag +{ + self = [super init]; + if (self) { + if (data) { + _buffer = data; + _startOffset = startOffset; + _bufferOwner = NO; + _originalBufferLength = data.length; + } else { + _buffer = [[NSMutableData alloc] initWithLength:MAX(0, fixedLength)]; + _startOffset = 0; + _bufferOwner = YES; + _originalBufferLength = 0; + } + + _bytesRead = 0; + _maxLength = maxLength; + _timeout = timeout; + _fixedLength = fixedLength; + _tag = tag; + } + return self; +} + +/** + Returns the safe length of data that can be read relative to the buffer. + */ +- (NSUInteger)safeReadLength +{ + if (_fixedLength > 0) { + return _fixedLength - _bytesRead; + } else { + NSUInteger result = READ_CHUNK_SIZE; + + if (_maxLength > 0) { + result = MIN(result, (_maxLength - _bytesRead)); + } + + if (!_bufferOwner && _buffer.length == _originalBufferLength) { + NSUInteger bufferSize = _buffer.length; + NSUInteger bufferSpace = bufferSize - _startOffset - _bytesRead; + + if (bufferSpace > 0) { + result = MIN(result, bufferSpace); + } + } + + return result; + } +} + +@end + + +/** + Encompasses the instructions for any given write operation. +*/ +@interface SPDYSocketWriteOp : NSObject { + @public + NSData *_buffer; + NSUInteger _bytesWritten; + NSTimeInterval _timeout; + long _tag; +} + +- (id)initWithData:(NSData *)data timeout:(NSTimeInterval)timeout tag:(long)tag; +@end + +@implementation SPDYSocketWriteOp + +- (id)initWithData:(NSData *)data timeout:(NSTimeInterval)timeout tag:(long)tag +{ + self = [super init]; + if (self) { + _buffer = data; + _bytesWritten = 0; + _timeout = timeout; + _tag = tag; + } + return self; +} + +@end + + +/** + Encompasses instructions for TLS. +*/ +@interface SPDYSocketTLSOp : NSObject { + @public + NSDictionary *_tlsSettings; +} + +- (id)initWithTLSSettings:(NSDictionary *)settings; +@end + +@implementation SPDYSocketTLSOp + +- (id)initWithTLSSettings:(NSDictionary *)settings +{ + self = [super init]; + if (self) { + _tlsSettings = [settings copy]; + } + return self; +} + +@end + + +@implementation SPDYSocket +{ + CFSocketNativeHandle _socket4FD; + CFSocketNativeHandle _socket6FD; + + CFSocketRef _socket4; // IPv4 + CFSocketRef _socket6; // IPv6 + + CFReadStreamRef _readStream; + CFWriteStreamRef _writeStream; + + CFRunLoopSourceRef _source4; // For _socket4 + CFRunLoopSourceRef _source6; // For _socket6 + CFRunLoopRef _runLoop; + CFSocketContext _context; + NSArray *_runLoopModes; + + NSTimer *_connectTimer; + + NSMutableArray *_readQueue; + SPDYSocketReadOp *_currentReadOp; + NSTimer *_readTimer; + NSMutableData *_unreadData; + + NSMutableArray *_writeQueue; + SPDYSocketWriteOp *_currentWriteOp; + NSTimer *_writeTimer; + + __weak id _delegate; + uint16_t _flags; +} + +- (id)init +{ + return [self initWithDelegate:nil]; +} + +- (id)initWithDelegate:(id)delegate +{ + self = [super init]; + if (self) { + _delegate = delegate; + _flags = (uint16_t)0; + _socket4FD = 0; + _socket6FD = 0; + _readQueue = [[NSMutableArray alloc] initWithCapacity:READ_QUEUE_CAPACITY]; + _writeQueue = [[NSMutableArray alloc] initWithCapacity:WRITE_QUEUE_CAPACITY]; + _runLoopModes = @[NSDefaultRunLoopMode]; + + NSAssert(sizeof(CFSocketContext) == sizeof(CFStreamClientContext), @"CFSocketContext != CFStreamClientContext"); + _context.version = 0; + _context.info = (__bridge void *)(self); + _context.retain = nil; + _context.release = nil; + _context.copyDescription = nil; + } + return self; +} + +- (void)dealloc +{ + [self _close]; + [NSObject cancelPreviousPerformRequestsWithTarget:self]; +} + + +#pragma mark Accessors + +- (id)delegate +{ + CHECK_THREAD_SAFETY(); + + return _delegate; +} + +- (void)setDelegate:(id)delegate +{ + CHECK_THREAD_SAFETY(); + + _delegate = delegate; +} + +- (CFSocketRef)cfSocket +{ + CHECK_THREAD_SAFETY(); + + return _socket4 ?: _socket6; +} + +- (CFReadStreamRef)cfReadStream +{ + CHECK_THREAD_SAFETY(); + + return _readStream; +} + +- (CFWriteStreamRef)cfWriteStream +{ + CHECK_THREAD_SAFETY(); + + return _writeStream; +} + + +#pragma mark CFRunLoop scheduling + +- (void)_addSource:(CFRunLoopSourceRef)source +{ + for (NSString *runLoopMode in _runLoopModes) { + CFRunLoopAddSource(_runLoop, source, (__bridge CFStringRef)runLoopMode); + } +} + +- (void)_removeSource:(CFRunLoopSourceRef)source +{ + for (NSString *runLoopMode in _runLoopModes) { + CFRunLoopRemoveSource(_runLoop, source, (__bridge CFStringRef)runLoopMode); + } +} + +- (void)_addSource:(CFRunLoopSourceRef)source mode:(NSString *)runLoopMode +{ + CFRunLoopAddSource(_runLoop, source, (__bridge CFStringRef)runLoopMode); +} + +- (void)_removeSource:(CFRunLoopSourceRef)source mode:(NSString *)runLoopMode +{ + CFRunLoopRemoveSource(_runLoop, source, (__bridge CFStringRef)runLoopMode); +} + +- (void)_addTimer:(NSTimer *)timer +{ + for (NSString *runLoopMode in _runLoopModes) { + CFRunLoopAddTimer(_runLoop, (__bridge CFRunLoopTimerRef)timer, (__bridge CFStringRef)runLoopMode); + } +} + +- (void)_removeTimer:(NSTimer *)timer +{ + for (NSString *runLoopMode in _runLoopModes) { + CFRunLoopRemoveTimer(_runLoop, (__bridge CFRunLoopTimerRef)timer, (__bridge CFStringRef)runLoopMode); + } +} + +- (void)_addTimer:(NSTimer *)timer mode:(NSString *)runLoopMode +{ + CFRunLoopAddTimer(_runLoop, (__bridge CFRunLoopTimerRef)timer, (__bridge CFStringRef)runLoopMode); +} + +- (void)_removeTimer:(NSTimer *)timer mode:(NSString *)runLoopMode +{ + CFRunLoopRemoveTimer(_runLoop, (__bridge CFRunLoopTimerRef)timer, (__bridge CFStringRef)runLoopMode); +} + +- (void)_unscheduleReadStream +{ + for (NSString *runLoopMode in _runLoopModes) { + CFReadStreamUnscheduleFromRunLoop(_readStream, _runLoop, (__bridge CFStringRef)runLoopMode); + } + CFReadStreamSetClient(_readStream, kCFStreamEventNone, NULL, NULL); +} + +- (void)_unscheduleWriteStream +{ + for (NSString *runLoopMode in _runLoopModes) { + CFWriteStreamUnscheduleFromRunLoop(_writeStream, _runLoop, (__bridge CFStringRef)runLoopMode); + } + CFWriteStreamSetClient(_writeStream, kCFStreamEventNone, NULL, NULL); +} + + +#pragma mark Configuration + +- (bool)setRunLoop:(NSRunLoop *)runLoop +{ + NSAssert(_runLoop == NULL || _runLoop == CFRunLoopGetCurrent(), + @"moveToRunLoop must be called from within the current RunLoop!"); + + if (runLoop == nil) { + return NO; + } + if (_runLoop == [runLoop getCFRunLoop]) { + return YES; + } + + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + _flags &= ~kDequeueReadScheduled; + _flags &= ~kDequeueWriteScheduled; + + if (_readStream && _writeStream) { + [self _unscheduleReadStream]; + [self _unscheduleWriteStream]; + } + + if (_source4) [self _removeSource:_source4]; + if (_source6) [self _removeSource:_source6]; + + if (_readTimer) [self _removeTimer:_readTimer]; + if (_writeTimer) [self _removeTimer:_writeTimer]; + + _runLoop = [runLoop getCFRunLoop]; + + if (_readTimer) [self _addTimer:_readTimer]; + if (_writeTimer) [self _addTimer:_writeTimer]; + + if (_source4) [self _addSource:_source4]; + if (_source6) [self _addSource:_source6]; + + if (_readStream && _writeStream) { + if (![self _scheduleStreamsOnRunLoop:runLoop error:nil]) { + return NO; + } + } + + [runLoop performSelector:@selector(_dequeueRead) target:self argument:nil order:0 modes:_runLoopModes]; + [runLoop performSelector:@selector(_dequeueWrite) target:self argument:nil order:0 modes:_runLoopModes]; + [runLoop performSelector:@selector(_scheduleDisconnect) target:self argument:nil order:0 modes:_runLoopModes]; + + return YES; +} + +- (bool)setRunLoopModes:(NSArray *)runLoopModes +{ + NSAssert(_runLoop == NULL || _runLoop == CFRunLoopGetCurrent(), + @"setRunLoopModes must be called from within the current RunLoop!"); + + if (runLoopModes.count == 0) { + return NO; + } + if ([_runLoopModes isEqualToArray:runLoopModes]) { + return YES; + } + + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + _flags &= ~kDequeueReadScheduled; + _flags &= ~kDequeueWriteScheduled; + + if (_readStream && _writeStream) { + [self _unscheduleReadStream]; + [self _unscheduleWriteStream]; + } + + if (_source4) [self _removeSource:_source4]; + if (_source6) [self _removeSource:_source6]; + + if (_readTimer) [self _removeTimer:_readTimer]; + if (_writeTimer) [self _removeTimer:_writeTimer]; + + _runLoopModes = [runLoopModes copy]; + + if (_readTimer) [self _addTimer:_readTimer]; + if (_writeTimer) [self _addTimer:_writeTimer]; + + if (_source4) [self _addSource:_source4]; + if (_source6) [self _addSource:_source6]; + + if (_readStream && _writeStream) { + if (![self _scheduleStreamsOnRunLoop:nil error:nil]) { + return NO; + } + } + + [self performSelector:@selector(_dequeueRead) withObject:nil afterDelay:0 inModes:_runLoopModes]; + [self performSelector:@selector(_dequeueWrite) withObject:nil afterDelay:0 inModes:_runLoopModes]; + [self performSelector:@selector(_scheduleDisconnect) withObject:nil afterDelay:0 inModes:_runLoopModes]; + + return YES; +} + +- (bool)addRunLoopMode:(NSString *)runLoopMode +{ + NSAssert(_runLoop == NULL || _runLoop == CFRunLoopGetCurrent(), + @"addRunLoopMode must be called from within the current RunLoop!"); + + if (runLoopMode == nil) { + return NO; + } + if ([_runLoopModes containsObject:runLoopMode]) { + return YES; + } + + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + _flags &= ~kDequeueReadScheduled; + _flags &= ~kDequeueWriteScheduled; + + NSArray *newRunLoopModes = [_runLoopModes arrayByAddingObject:runLoopMode]; + _runLoopModes = newRunLoopModes; + + if (_readTimer) [self _addTimer:_readTimer mode:runLoopMode]; + if (_writeTimer) [self _addTimer:_writeTimer mode:runLoopMode]; + + if (_source4) [self _addSource:_source4 mode:runLoopMode]; + if (_source6) [self _addSource:_source6 mode:runLoopMode]; + + if (_readStream && _writeStream) { + CFReadStreamScheduleWithRunLoop(_readStream, CFRunLoopGetCurrent(), (__bridge CFStringRef)runLoopMode); + CFWriteStreamScheduleWithRunLoop(_writeStream, CFRunLoopGetCurrent(), (__bridge CFStringRef)runLoopMode); + } + + [self performSelector:@selector(_dequeueRead) withObject:nil afterDelay:0 inModes:_runLoopModes]; + [self performSelector:@selector(_dequeueWrite) withObject:nil afterDelay:0 inModes:_runLoopModes]; + [self performSelector:@selector(_scheduleDisconnect) withObject:nil afterDelay:0 inModes:_runLoopModes]; + + return YES; +} + +- (bool)removeRunLoopMode:(NSString *)runLoopMode +{ + NSAssert(_runLoop == NULL || _runLoop == CFRunLoopGetCurrent(), + @"addRunLoopMode must be called from within the current RunLoop!"); + + if (runLoopMode == nil) { + return NO; + } + if (![_runLoopModes containsObject:runLoopMode]) { + return YES; + } + + NSMutableArray *newRunLoopModes = [_runLoopModes mutableCopy]; + [newRunLoopModes removeObject:runLoopMode]; + + if (newRunLoopModes.count == 0) { + return NO; + } + + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + _flags &= ~kDequeueReadScheduled; + _flags &= ~kDequeueWriteScheduled; + + _runLoopModes = [newRunLoopModes copy]; + + if (_readTimer) [self _removeTimer:_readTimer mode:runLoopMode]; + if (_writeTimer) [self _removeTimer:_writeTimer mode:runLoopMode]; + + if (_source4) [self _removeSource:_source4 mode:runLoopMode]; + if (_source6) [self _removeSource:_source6 mode:runLoopMode]; + + if (_readStream && _writeStream) { + CFReadStreamScheduleWithRunLoop(_readStream, CFRunLoopGetCurrent(), (__bridge CFStringRef)runLoopMode); + CFWriteStreamScheduleWithRunLoop(_writeStream, CFRunLoopGetCurrent(), (__bridge CFStringRef)runLoopMode); + } + + [self performSelector:@selector(_dequeueRead) withObject:nil afterDelay:0 inModes:_runLoopModes]; + [self performSelector:@selector(_dequeueWrite) withObject:nil afterDelay:0 inModes:_runLoopModes]; + [self performSelector:@selector(_scheduleDisconnect) withObject:nil afterDelay:0 inModes:_runLoopModes]; + + return YES; +} + +- (NSArray *)runLoopModes +{ + CHECK_THREAD_SAFETY(); + + return _runLoopModes; +} + + +#pragma mark Connecting + + +- (bool)connectToHost:(NSString *)hostname onPort:(in_port_t)port error:(NSError **)pError +{ + return [self connectToHost:hostname onPort:port withTimeout:-1 error:pError]; +} + +/** + Attempts to connect to the given host and port. + + The delegate will have access to the CFReadStream and CFWriteStream prior to connection, + specifically in the socketWillConnect: method. +*/ +- (bool)connectToHost:(NSString *)hostname + onPort:(in_port_t)port + withTimeout:(NSTimeInterval)timeout + error:(NSError **)pError +{ + if (_delegate == nil) { + [NSException raise:SPDYSocketException + format:@"Attempting to connect without a delegate. Set a delegate first."]; + } + + if (![self _fullyDisconnected]) { + [NSException raise:SPDYSocketException + format:@"Attempting to connect while connected or accepting connections. Disconnect first."]; + } + + [self _emptyQueues]; + + if (![self _createStreamsToHost:hostname onPort:port error:pError]) goto Failed; + if (![self _scheduleStreamsOnRunLoop:nil error:pError]) goto Failed; + if (![self _configureStreams:pError]) goto Failed; + if (![self _openStreams:pError]) goto Failed; + + [self _startConnectTimeout:timeout]; + _flags |= kDidStartDelegate; + + return YES; + + Failed: + [self _close]; + return NO; +} + +- (void)_startConnectTimeout:(NSTimeInterval)timeout +{ + if (timeout >= 0.0) { + _connectTimer = [NSTimer timerWithTimeInterval:timeout + target:self + selector:@selector(_timeoutConnect:) + userInfo:nil + repeats:NO]; + [self _addTimer:_connectTimer]; + } +} + +- (void)_endConnectTimeout +{ + [_connectTimer invalidate]; + _connectTimer = nil; +} + +- (void)_timeoutConnect:(NSTimer *)timer +{ +#pragma unused(timer) + + [self _endConnectTimeout]; + [self _closeWithError:[self connectTimeoutError]]; +} + + +#pragma mark CFStream management + +/** + Creates the CFReadStream and CFWriteStream from the given hostname and port number. + + The CFSocket may be extracted from either stream after the streams have been opened. +*/ +- (bool)_createStreamsToHost:(NSString *)hostname onPort:(in_port_t)port error:(NSError **)pError +{ + CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)hostname, port, &_readStream, &_writeStream); + if (_readStream == NULL || _writeStream == NULL) { + if (pError) *pError = [self streamError]; + return NO; + } + + CFReadStreamSetProperty(_readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); + CFWriteStreamSetProperty(_writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue); + + return YES; +} + +- (bool)_scheduleStreamsOnRunLoop:(NSRunLoop *)runLoop error:(NSError **)pError +{ + _runLoop = runLoop ? [runLoop getCFRunLoop] : CFRunLoopGetCurrent(); + + CFOptionFlags readStreamEvents = + kCFStreamEventHasBytesAvailable | + kCFStreamEventErrorOccurred | + kCFStreamEventEndEncountered | + kCFStreamEventOpenCompleted; + + if (!CFReadStreamSetClient(_readStream, + readStreamEvents, + (CFReadStreamClientCallBack)&SPDYSocketCFReadStreamCallback, + (CFStreamClientContext *)(&_context))) + { + NSError *error = [self streamError]; + SPDY_WARNING(@"%@ couldn't attach read stream to run loop: %@,", self, error); + + if (pError) *pError = error; + return NO; + } + + CFOptionFlags writeStreamEvents = + kCFStreamEventCanAcceptBytes | + kCFStreamEventErrorOccurred | + kCFStreamEventEndEncountered | + kCFStreamEventOpenCompleted; + + if (!CFWriteStreamSetClient(_writeStream, + writeStreamEvents, + (CFWriteStreamClientCallBack)&SPDYSocketCFWriteStreamCallback, + (CFStreamClientContext *)(&_context))) + { + NSError *error = [self streamError]; + SPDY_WARNING(@"%@ couldn't attach write stream to run loop: %@,", self, error); + + if (pError) *pError = error; + return NO; + } + + for (NSString *runLoopMode in _runLoopModes) { + CFReadStreamScheduleWithRunLoop(_readStream, _runLoop, (__bridge CFStringRef)runLoopMode); + CFWriteStreamScheduleWithRunLoop(_writeStream, _runLoop, (__bridge CFStringRef)runLoopMode); + } + + return YES; +} + +/** + Allows the delegate method to configure the read and/or write streams prior to connection. + + The CFSocket and CFNativeSocket will not be available until after the connection is opened. +*/ +- (bool)_configureStreams:(NSError **)pError +{ + if ([_delegate respondsToSelector:@selector(socketWillConnect:)]) { + if (![_delegate socketWillConnect:self]) { + if (pError) *pError = [self abortError]; + return NO; + } + } + return YES; +} + +- (bool)_openStreams:(NSError **)pError +{ + bool success = YES; + + if (success && !CFReadStreamOpen(_readStream)) { + SPDY_WARNING(@"%@ couldn't open read stream,", self); + success = NO; + } + + if (success && !CFWriteStreamOpen(_writeStream)) { + SPDY_WARNING(@"%@ couldn't open write stream,", self); + success = NO; + } + + if (!success) { + if (pError) *pError = [self streamError]; + } + + return success; +} + +/** + Called when read or write streams open. +*/ +- (void)_onStreamOpened +{ + if ((_flags & kDidCompleteOpenForRead) && (_flags & kDidCompleteOpenForWrite)) { + NSError *error = nil; + + if (![self _setSocketViaStreams:&error]) { + SPDY_ERROR(@"%@ couldn't get socket from streams, %@. Disconnecting.", self, error); + [self _closeWithError:error]; + return; + } + + [self _endConnectTimeout]; + [self _setConnectionProperties]; + + if ([_delegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) { + [_delegate socket:self didConnectToHost:_connectedHost port:_connectedPort]; + } + + [self _dequeueRead]; + [self _dequeueWrite]; + } +} + +- (bool)_setSocketViaStreams:(NSError **)pError +{ + CFSocketNativeHandle native; + CFDataRef nativeProp = CFReadStreamCopyProperty(_readStream, kCFStreamPropertySocketNativeHandle); + if (nativeProp == NULL) { + if (pError) *pError = [self streamError]; + return NO; + } + + CFIndex length = MIN(CFDataGetLength(nativeProp), (CFIndex)sizeof(native)); + CFDataGetBytes(nativeProp, CFRangeMake(0, length), (uint8_t *)&native); + CFRelease(nativeProp); + + CFSocketRef socket = CFSocketCreateWithNative(kCFAllocatorDefault, native, 0, NULL, NULL); + if (socket == NULL) { + if (pError) *pError = [self socketError]; + return NO; + } + + CFDataRef peeraddr = CFSocketCopyPeerAddress(socket); + if (peeraddr == NULL) { + SPDY_ERROR(@"%@ couldn't determine IP version of socket", self); + + CFRelease(socket); + + if (pError) *pError = [self socketError]; + return NO; + } + struct sockaddr *sa = (struct sockaddr *)CFDataGetBytePtr(peeraddr); + + if (sa->sa_family == AF_INET) { + _socket4 = socket; + _socket4FD = native; + } else { + _socket6 = socket; + _socket6FD = native; + } + + CFRelease(peeraddr); + + return YES; +} + + +#pragma mark Disconnect Implementation + +- (void)_closeWithError:(NSError *)error +{ + _flags |= kClosingWithError; + + if (_flags & kDidStartDelegate) { + [self _captureUnreadData]; + + // Give the delegate the opportunity to recover unread data + if ([_delegate respondsToSelector:@selector(socket:willDisconnectWithError:)]) { + [_delegate socket:self willDisconnectWithError:error]; + } + } + [self _close]; +} + +- (void)_captureUnreadData +{ + if (_currentReadOp && + [_currentReadOp isKindOfClass:[SPDYSocketReadOp class]] && + _currentReadOp->_bytesRead > 0) + { + void const *buffer = _currentReadOp->_buffer.mutableBytes + _currentReadOp->_startOffset; + _unreadData = [[NSMutableData alloc] initWithBytes:buffer + length:_currentReadOp->_bytesRead]; + } + + [self _emptyQueues]; +} + +- (void)_emptyQueues +{ + if (_currentReadOp) [self _endRead]; + if (_currentWriteOp) [self _endWrite]; + + [_readQueue removeAllObjects]; + [_writeQueue removeAllObjects]; + + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_dequeueRead) object:nil]; + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_dequeueWrite) object:nil]; + + _flags &= ~kDequeueReadScheduled; + _flags &= ~kDequeueWriteScheduled; +} + +/** + Disconnects. This is called for both error and clean disconnections. +*/ +- (void)_close +{ + [self _emptyQueues]; + + _unreadData = nil; + + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(disconnect) object:nil]; + + if (_connectTimer) { + [self _endConnectTimeout]; + } + + if (_readStream) { + [self _unscheduleReadStream]; + CFReadStreamClose(_readStream); + CFRelease(_readStream); + _readStream = NULL; + } + + if (_writeStream) { + [self _unscheduleWriteStream]; + CFWriteStreamClose(_writeStream); + CFRelease(_writeStream); + _writeStream = NULL; + } + + if (_socket4) { + CFSocketInvalidate(_socket4); + CFRelease(_socket4); + _socket4 = NULL; + } + + if (_socket6) { + CFSocketInvalidate(_socket6); + CFRelease(_socket6); + _socket6 = NULL; + } + + // Closing the streams or sockets resulted in closing the underlying native socket + _socket4FD = 0; + _socket6FD = 0; + + if (_source4) { + [self _removeSource:_source4]; + CFRelease(_source4); + _source4 = NULL; + } + + if (_source6) { + [self _removeSource:_source6]; + CFRelease(_source6); + _source6 = NULL; + } + _runLoop = NULL; + + // If the connection has at least been started, notify delegate that it is now ending + bool shouldCallDelegate = (_flags & kDidStartDelegate) == kDidStartDelegate; + + // Clear all flags + _flags = (uint16_t)0; + + if (shouldCallDelegate) { + if ([_delegate respondsToSelector:@selector(socketDidDisconnect:)]) { + [_delegate socketDidDisconnect:self]; + } + } + + // Do not access any instance variables after calling onSocketDidDisconnect. + // This gives the delegate freedom to release us without returning here and crashing. +} + +/** + Disconnects immediately. Any pending reads or writes are dropped. +*/ +- (void)disconnect +{ + CHECK_THREAD_SAFETY(); + + [self _close]; +} + +/** + Diconnects after all pending reads have completed. +*/ +- (void)disconnectAfterReads +{ + CHECK_THREAD_SAFETY(); + + _flags |= (kForbidReadsWrites | kDisconnectAfterReads); + [self _scheduleDisconnect]; +} + +/** + Disconnects after all pending writes have completed. +*/ +- (void)disconnectAfterWrites +{ + CHECK_THREAD_SAFETY(); + + _flags |= (kForbidReadsWrites | kDisconnectAfterWrites); + [self _scheduleDisconnect]; +} + +/** + Disconnects after all pending reads and writes have completed. +*/ +- (void)disconnectAfterReadsAndWrites +{ + CHECK_THREAD_SAFETY(); + + _flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); + [self _scheduleDisconnect]; +} + +- (void)_scheduleDisconnect +{ + bool shouldDisconnect = NO; + + if (_flags & kDisconnectAfterReads) { + if (_readQueue.count == 0 && _currentReadOp == nil) { + if (_flags & kDisconnectAfterWrites) { + if (_writeQueue.count == 0 && _currentWriteOp == nil) { + shouldDisconnect = YES; + } + } else { + shouldDisconnect = YES; + } + } + } else if (_flags & kDisconnectAfterWrites) { + if (_writeQueue.count == 0 && _currentWriteOp == nil) { + shouldDisconnect = YES; + } + } + + if (shouldDisconnect) { + [self performSelector:@selector(disconnect) withObject:nil afterDelay:0 inModes:_runLoopModes]; + } +} + +/** + In the event of an error, this method may be called during socket:willDisconnectWithError: to read + any data that's left on the socket. +*/ +- (NSData *)unreadData +{ + CHECK_THREAD_SAFETY(); + + if (!(_flags & kClosingWithError)) return nil; + + if (_readStream == NULL) return nil; + + NSUInteger totalBytesRead = _unreadData.length; + + bool error = NO; + while (!error && CFReadStreamHasBytesAvailable(_readStream)) { + if (totalBytesRead == _unreadData.length) { + [_unreadData increaseLengthBy:READ_CHUNK_SIZE]; + } + + NSUInteger bytesToRead = _unreadData.length - totalBytesRead; + uint8_t *readBuffer = (uint8_t *)(_unreadData.mutableBytes + totalBytesRead); + + CFIndex bytesRead = CFReadStreamRead(_readStream, readBuffer, bytesToRead); + + if (bytesRead < 0) { + error = YES; + } else { + totalBytesRead += bytesRead; + } + } + + [_unreadData setLength:totalBytesRead]; + + return _unreadData; +} + + +#pragma mark Errors + +- (NSError *)socketError +{ + NSDictionary *info = @{ NSLocalizedDescriptionKey : @"general CFSocket error" }; + return [NSError errorWithDomain:SPDYSocketErrorDomain code:SPDYSocketCFSocketError userInfo:info]; +} + +- (NSError *)streamError +{ + CFErrorRef error; + if (_readStream) { + error = CFReadStreamCopyError(_readStream); + if (error) return CFBridgingRelease(error); + } + + if (_writeStream) { + error = CFWriteStreamCopyError(_writeStream); + if (error) return CFBridgingRelease(error); + } + + return nil; +} + +- (NSError *)abortError +{ + NSDictionary *info = @{ NSLocalizedDescriptionKey : @"The socket connection was canceled." }; + return [NSError errorWithDomain:SPDYSocketErrorDomain code:SPDYSocketConnectCanceled userInfo:info]; +} + +- (NSError *)connectTimeoutError +{ + NSDictionary *info = @{ NSLocalizedDescriptionKey : @"The socket connection timed out." }; + return [NSError errorWithDomain:SPDYSocketErrorDomain code:SPDYSocketConnectTimeout userInfo:info]; +} + +- (NSError *)readTimeoutError +{ + NSDictionary *info = @{ NSLocalizedDescriptionKey : @"The read operation timed out." }; + return [NSError errorWithDomain:SPDYSocketErrorDomain code:SPDYSocketReadTimeout userInfo:info]; +} + +- (NSError *)writeTimeoutError +{ + NSDictionary *info = @{ NSLocalizedDescriptionKey : @"The write operation timed out." }; + return [NSError errorWithDomain:SPDYSocketErrorDomain code:SPDYSocketWriteTimeout userInfo:info]; +} + + +#pragma mark State + +- (bool)connected +{ + CHECK_THREAD_SAFETY(); + + CFStreamStatus status; + + if (_readStream) { + status = CFReadStreamGetStatus(_readStream); + if (status != kCFStreamStatusOpen && + status != kCFStreamStatusReading && + status != kCFStreamStatusError) + return NO; + } else { + return NO; + } + + if (_writeStream) { + status = CFWriteStreamGetStatus(_writeStream); + if (status != kCFStreamStatusOpen && + status != kCFStreamStatusWriting && + status != kCFStreamStatusError) + return NO; + } else { + return NO; + } + + return YES; +} + +- (bool)_fullyDisconnected +{ + CHECK_THREAD_SAFETY(); + + return _socket4FD == 0 && + _socket6FD == 0 && + _socket4 == NULL && + _socket6 == NULL && + _readStream == NULL && + _writeStream == NULL; +} + +- (void)_setConnectionProperties +{ + CHECK_THREAD_SAFETY(); + + char addrBuf[INET6_ADDRSTRLEN]; + + if (_socket4FD > 0) { + + struct sockaddr_in sockaddr4; + struct sockaddr_in *pSockaddr4 = &sockaddr4; + socklen_t sockaddr4len = sizeof(sockaddr4); + + if (getpeername(_socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) >= 0 && + inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf))) + { + _connectedPort = ntohs(sockaddr4.sin_port); + _connectedHost = [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; + return; + } + + } else if (_socket6FD > 0) { + + struct sockaddr_in6 sockaddr6; + struct sockaddr_in6 *pSockaddr6 = &sockaddr6; + socklen_t sockaddr6len = sizeof(sockaddr6); + + if (getpeername(_socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) >= 0 && + inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf))) + { + _connectedPort = ntohs(sockaddr6.sin6_port); + _connectedHost = [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; + return; + } + + } + + _connectedPort = 0; + _connectedHost = nil; +} + +- (in_port_t)connectedPort +{ + CHECK_THREAD_SAFETY(); + + return _connectedPort; +} + +- (NSString *)connectedHost +{ + CHECK_THREAD_SAFETY(); + + return _connectedHost; +} + +- (bool)isIPv4 +{ + CHECK_THREAD_SAFETY(); + + return (_socket4FD > 0 || _socket4 != NULL); +} + +- (bool)isIPv6 +{ + CHECK_THREAD_SAFETY(); + + return (_socket6FD > 0 || _socket6 != NULL); +} + + +#pragma mark Reading + +- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; +} + +- (void)readDataWithTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + maxLength:(NSUInteger)length + tag:(long)tag +{ + CHECK_THREAD_SAFETY(); + + if (offset > buffer.length) return; + if (_flags & kForbidReadsWrites) return; + + SPDYSocketReadOp *readOp = [[SPDYSocketReadOp alloc] initWithData:buffer + startOffset:offset + maxLength:length + timeout:timeout + fixedLength:0 + tag:tag]; + [_readQueue addObject:readOp]; + [self _scheduleRead]; +} + +- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + [self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag]; +} + +- (void)readDataToLength:(NSUInteger)length + withTimeout:(NSTimeInterval)timeout + buffer:(NSMutableData *)buffer + bufferOffset:(NSUInteger)offset + tag:(long)tag +{ + CHECK_THREAD_SAFETY(); + + if (length == 0) return; + if (offset > buffer.length) return; + if (_flags & kForbidReadsWrites) return; + + SPDYSocketReadOp *readOp = [[SPDYSocketReadOp alloc] initWithData:buffer + startOffset:offset + maxLength:0 + timeout:timeout + fixedLength:length + tag:tag]; + [_readQueue addObject:readOp]; + [self _scheduleRead]; +} + +- (void)_scheduleRead +{ + if ((_flags & kDequeueReadScheduled) == 0) { + _flags |= kDequeueReadScheduled; + [self performSelector:@selector(_dequeueRead) withObject:nil afterDelay:0 inModes:_runLoopModes]; + } +} + +- (void)_dequeueRead +{ + _flags &= ~kDequeueReadScheduled; + + if (_readStream && _currentReadOp == nil) { + if (_readQueue.count > 0) { + _currentReadOp = [_readQueue objectAtIndex:0]; + [_readQueue removeObjectAtIndex:0]; + + if ([_currentReadOp isKindOfClass:[SPDYSocketTLSOp class]]) { + _flags |= kStartingReadTLS; + + [self _tryTLSHandshake]; + } else { + if (_currentReadOp->_timeout >= 0.0) { + _readTimer = [NSTimer timerWithTimeInterval:_currentReadOp->_timeout + target:self + selector:@selector(_timeoutRead:) + userInfo:nil + repeats:NO]; + [self _addTimer:_readTimer]; + } + + [self _read]; + } + } else if (_flags & kDisconnectAfterReads) { + if (_flags & kDisconnectAfterWrites) { + if (_writeQueue.count == 0 && _currentWriteOp == nil) { + [self disconnect]; + } + } else { + [self disconnect]; + } + } + } +} + +- (bool)_readStreamReady +{ + return (_flags & kSocketHasBytesAvailable) || CFReadStreamHasBytesAvailable(_readStream); +} + +- (void)_read +{ + if (_currentReadOp == nil || _readStream == NULL) { + return; + } + + NSError *readError = nil; + NSUInteger newBytesRead = 0; + bool readComplete = NO; + + while(!readComplete && !readError && [self _readStreamReady]) { + + NSUInteger bytesToRead = [_currentReadOp safeReadLength]; + NSUInteger bufferSize = _currentReadOp->_buffer.length; + NSUInteger bufferSpace = bufferSize - _currentReadOp->_startOffset - _currentReadOp->_bytesRead; + + if (bytesToRead > bufferSpace) { + [_currentReadOp->_buffer increaseLengthBy:(bytesToRead - bufferSpace)]; + } + + uint8_t *readIndex = (uint8_t *)(_currentReadOp->_buffer.mutableBytes + + _currentReadOp->_startOffset + + _currentReadOp->_bytesRead); + + CFIndex bytesRead = CFReadStreamRead(_readStream, readIndex, bytesToRead); + _flags &= ~kSocketHasBytesAvailable; + + if (bytesRead < 0) { + readError = [self streamError]; + } else { + _currentReadOp->_bytesRead += bytesRead; + newBytesRead += bytesRead; + + if (_currentReadOp->_fixedLength > 0) { + readComplete = (_currentReadOp->_bytesRead == _currentReadOp->_fixedLength); + } else if (_currentReadOp->_maxLength > 0) { + readComplete = (_currentReadOp->_bytesRead >= _currentReadOp->_maxLength); + } + } + } + + if (_currentReadOp->_fixedLength <= 0 && _currentReadOp->_bytesRead > 0) { + readComplete = YES; + } + + if (readComplete) { + [self _finishRead]; + if (!readError) [self _scheduleRead]; + } else if (newBytesRead > 0) { + if ([_delegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)]) { + [_delegate socket:self didReadPartialDataOfLength:newBytesRead tag:_currentReadOp->_tag]; + } + } + + if (readError) { + [self _closeWithError:readError]; + } +} + +- (void)_finishRead +{ + NSAssert(_currentReadOp, @"Trying to complete current read when there is no current read."); + + NSData *readData; + + if (_currentReadOp->_bufferOwner) { + // We created the buffer so it's safe to trim it + [_currentReadOp->_buffer setLength:_currentReadOp->_bytesRead]; + readData = _currentReadOp->_buffer; + } else { + // The caller owns the buffer, so only trim if we increased its size + if (_currentReadOp->_buffer.length > _currentReadOp->_originalBufferLength) { + NSUInteger readLength = _currentReadOp->_startOffset + _currentReadOp->_bytesRead; + NSUInteger trimmedLength = MAX(readLength, _currentReadOp->_originalBufferLength); + [_currentReadOp->_buffer setLength:trimmedLength]; + } + + void *buffer = _currentReadOp->_buffer.mutableBytes + _currentReadOp->_startOffset; + + readData = [NSData dataWithBytesNoCopy:buffer length:_currentReadOp->_bytesRead freeWhenDone:NO]; + } + + if ([_delegate respondsToSelector:@selector(socket:didReadData:withTag:)]) { + [_delegate socket:self didReadData:readData withTag:_currentReadOp->_tag]; + } + + // Caller may have disconnected in the above delegate method + if (_currentReadOp != nil) { + [self _endRead]; + } +} + +- (void)_endRead +{ + NSAssert(_currentReadOp, @"Trying to end current read when there is no current read."); + + [_readTimer invalidate]; + _readTimer = nil; + + _currentReadOp = nil; +} + +- (void)_timeoutRead:(NSTimer *)timer +{ +#pragma unused(timer) + + NSTimeInterval timeoutExtension = 0.0; + + if ([_delegate respondsToSelector:@selector(socket:willTimeoutReadWithTag:elapsed:bytesDone:)]) { + timeoutExtension = [_delegate socket:self willTimeoutReadWithTag:_currentReadOp->_tag + elapsed:_currentReadOp->_timeout + bytesDone:_currentReadOp->_bytesRead]; + } + + if (timeoutExtension > 0.0) { + _currentReadOp->_timeout += timeoutExtension; + + _readTimer = [NSTimer timerWithTimeInterval:timeoutExtension + target:self + selector:@selector(_timeoutRead:) + userInfo:nil + repeats:NO]; + [self _addTimer:_readTimer]; + } else { + // Do not call _endRead here. + // We must allow the delegate access to any partial read in the unreadData method. + + [self _closeWithError:[self readTimeoutError]]; + } +} + + +#pragma mark Writing + +- (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag +{ + CHECK_THREAD_SAFETY(); + + if (data == nil || data.length == 0) return; + if (_flags & kForbidReadsWrites) return; + + SPDYSocketWriteOp *writeOp = [[SPDYSocketWriteOp alloc] initWithData:data timeout:timeout tag:tag]; + + [_writeQueue addObject:writeOp]; + [self _scheduleWrite]; + +} + +- (void)_scheduleWrite +{ + if ((_flags & kDequeueWriteScheduled) == 0) { + _flags |= kDequeueWriteScheduled; + [self performSelector:@selector(_dequeueWrite) withObject:nil afterDelay:0 inModes:_runLoopModes]; + } +} + +- (void)_dequeueWrite +{ + _flags &= ~kDequeueWriteScheduled; + + if (_writeStream && _currentWriteOp == nil) { + if (_writeQueue.count > 0) { + _currentWriteOp = [_writeQueue objectAtIndex:0]; + [_writeQueue removeObjectAtIndex:0]; + + if ([_currentWriteOp isKindOfClass:[SPDYSocketTLSOp class]]) { + _flags |= kStartingWriteTLS; + + [self _tryTLSHandshake]; + } else { + if (_currentWriteOp->_timeout >= 0.0) { + _writeTimer = [NSTimer timerWithTimeInterval:_currentWriteOp->_timeout + target:self + selector:@selector(_timeoutWrite:) + userInfo:nil + repeats:NO]; + [self _addTimer:_writeTimer]; + } + + [self _write]; + } + } else if (_flags & kDisconnectAfterWrites) { + if (_flags & kDisconnectAfterReads) { + if (_readQueue.count == 0 && _currentReadOp == nil) { + [self disconnect]; + } + } else { + [self disconnect]; + } + } + } +} + +- (bool)_writeStreamReady +{ + return (_flags & kSocketCanAcceptBytes) || CFWriteStreamCanAcceptBytes(_writeStream); +} + +- (void)_write +{ + if (_currentWriteOp == nil || _writeStream == NULL) { + return; + } + + NSUInteger newBytesWritten = 0; + bool writeComplete = NO; + + while (!writeComplete && [self _writeStreamReady]) { + + NSUInteger bytesRemaining = _currentWriteOp->_buffer.length - _currentWriteOp->_bytesWritten; + NSUInteger bytesToWrite = (bytesRemaining < WRITE_CHUNK_SIZE) ? bytesRemaining : WRITE_CHUNK_SIZE; + uint8_t *writeIndex = (uint8_t *)(_currentWriteOp->_buffer.bytes + _currentWriteOp->_bytesWritten); + + CFIndex bytesWritten = CFWriteStreamWrite(_writeStream, writeIndex, bytesToWrite); + _flags &= ~kSocketCanAcceptBytes; + + if (bytesWritten < 0) { + [self _closeWithError:[self streamError]]; + return; + } else { + _currentWriteOp->_bytesWritten += bytesWritten; + newBytesWritten += bytesWritten; + writeComplete = (_currentWriteOp->_buffer.length == _currentWriteOp->_bytesWritten); + } + } + + if (writeComplete) { + [self _finishWrite]; + [self _scheduleWrite]; + } else if (newBytesWritten > 0) { + if ([_delegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)]) { + [_delegate socket:self didWritePartialDataOfLength:newBytesWritten tag:_currentWriteOp->_tag]; + } + } +} + +- (void)_finishWrite +{ + NSAssert(_currentWriteOp, @"Trying to complete current write when there is no current write."); + + if ([_delegate respondsToSelector:@selector(socket:didWriteDataWithTag:)]) { + [_delegate socket:self didWriteDataWithTag:_currentWriteOp->_tag]; + } + + if (_currentWriteOp != nil) [self _endWrite]; // caller may have disconnected +} + +- (void)_endWrite +{ + NSAssert(_currentWriteOp, @"Trying to complete current write when there is no current write."); + + [_writeTimer invalidate]; + _writeTimer = nil; + + _currentWriteOp = nil; +} + +- (void)_timeoutWrite:(NSTimer *)timer +{ +#pragma unused(timer) + + NSTimeInterval timeoutExtension = 0.0; + + if ([_delegate respondsToSelector:@selector(socket:willTimeoutWriteWithTag:elapsed:bytesDone:)]) { + timeoutExtension = [_delegate socket:self willTimeoutWriteWithTag:_currentWriteOp->_tag + elapsed:_currentWriteOp->_timeout + bytesDone:_currentWriteOp->_bytesWritten]; + } + + if (timeoutExtension > 0.0) { + _currentWriteOp->_timeout += timeoutExtension; + + _writeTimer = [NSTimer timerWithTimeInterval:timeoutExtension + target:self + selector:@selector(_timeoutWrite:) + userInfo:nil + repeats:NO]; + [self _addTimer:_writeTimer]; + } else { + [self _closeWithError:[self writeTimeoutError]]; + } +} + + +#pragma mark TLS + +- (void)secureWithTLS:(NSDictionary *)tlsSettings +{ + CHECK_THREAD_SAFETY(); + + // apparently, using nil settings will prevent us from later being able to + // obtain the remote host's certificate via CFReadStreamCopyProperty(...) + SPDYSocketTLSOp *tlsOp = [[SPDYSocketTLSOp alloc] initWithTLSSettings:tlsSettings ?: @{}]; + + [_readQueue addObject:tlsOp]; + [self _scheduleRead]; + + [_writeQueue addObject:tlsOp]; + [self _scheduleWrite]; + +} + +/** + Starts TLS handshake only if all reads and writes queued prior to the call to + secureWithTLS: are complete. +*/ +- (void)_tryTLSHandshake +{ + if ((_flags & kStartingReadTLS) && (_flags & kStartingWriteTLS)) { + SPDYSocketTLSOp *tlsOp = (SPDYSocketTLSOp *)_currentReadOp; + + bool didStartOnReadStream = CFReadStreamSetProperty(_readStream, kCFStreamPropertySSLSettings, + (__bridge CFDictionaryRef)tlsOp->_tlsSettings); + bool didStartOnWriteStream = CFWriteStreamSetProperty(_writeStream, kCFStreamPropertySSLSettings, + (__bridge CFDictionaryRef)tlsOp->_tlsSettings); + + if (!didStartOnReadStream || !didStartOnWriteStream) { + [self _closeWithError:[self socketError]]; + } + } +} + +- (void)_onTLSHandshakeSuccess +{ + if ((_flags & kStartingReadTLS) && (_flags & kStartingWriteTLS)) { + _flags &= ~kStartingReadTLS; + _flags &= ~kStartingWriteTLS; + + bool acceptTrust = YES; + if ([_delegate respondsToSelector:@selector(socket:securedWithTrust:)]) { + SecTrustRef trust = (SecTrustRef)CFReadStreamCopyProperty(_readStream, kCFStreamPropertySSLPeerTrust); + acceptTrust = [_delegate socket:self securedWithTrust:trust]; + } + + if (!acceptTrust) { + [self _closeWithError:SPDY_SOCKET_ERROR(SPDYSocketTLSVerificationFailed, @"TLS trust verification failed.")]; + return; + } + + [self _endRead]; + [self _endWrite]; + + [self _scheduleRead]; + [self _scheduleWrite]; + } +} + + +#pragma mark CFReadStream callbacks + +- (void)handleCFReadStreamEvent:(CFStreamEventType)type forStream:(CFReadStreamRef)stream +{ +#pragma unused(stream) + + NSParameterAssert(_readStream != NULL); + + switch (type) { + case kCFStreamEventOpenCompleted: + _flags |= kDidCompleteOpenForRead; + [self _onStreamOpened]; + break; + case kCFStreamEventHasBytesAvailable: + if (_flags & kStartingReadTLS) { + [self _onTLSHandshakeSuccess]; + } else { + _flags |= kSocketHasBytesAvailable; + [self _read]; + } + break; + case kCFStreamEventErrorOccurred: + [self _closeWithError:[self streamError]]; + break; + case kCFStreamEventEndEncountered: + [self _closeWithError:SPDY_SOCKET_ERROR(SPDYSocketTransportError, @"Unexpected end of stream.")]; + break; + default: + SPDY_WARNING(@"%@ received unexpected CFReadStream callback, CFStreamEventType %li", self, type); + } +} + +- (void)handleCFWriteStreamEvent:(CFStreamEventType)type forStream:(CFWriteStreamRef)stream +{ +#pragma unused(stream) + + NSParameterAssert(_writeStream != NULL); + + switch (type) { + case kCFStreamEventOpenCompleted: + _flags |= kDidCompleteOpenForWrite; + [self _onStreamOpened]; + break; + case kCFStreamEventCanAcceptBytes: + if (_flags & kStartingWriteTLS) { + [self _onTLSHandshakeSuccess]; + } else { + _flags |= kSocketCanAcceptBytes; + [self _write]; + } + break; + case kCFStreamEventErrorOccurred: + case kCFStreamEventEndEncountered: + [self _closeWithError:[self streamError]]; + break; + default: + SPDY_WARNING(@"%@ received unexpected CFWriteStream callback, CFStreamEventType %li", self, type); + } +} + +static void SPDYSocketCFReadStreamCallback(CFReadStreamRef stream, CFStreamEventType type, void *pSocket) +{ + @autoreleasepool { + SPDYSocket * volatile spdySocket = (__bridge SPDYSocket *)pSocket; + [spdySocket handleCFReadStreamEvent:type forStream:stream]; + } +} + +static void SPDYSocketCFWriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType type, void *pSocket) +{ + @autoreleasepool { + SPDYSocket * volatile spdySocket = (__bridge SPDYSocket *)pSocket; + [spdySocket handleCFWriteStreamEvent:type forStream:stream]; + } +} + +@end diff --git a/SPDY/SPDYStream.h b/SPDY/SPDYStream.h new file mode 100644 index 0000000..9ef8dac --- /dev/null +++ b/SPDY/SPDYStream.h @@ -0,0 +1,51 @@ +// +// SPDYStream.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import +#import "SPDYDefinitions.h" + +@class SPDYProtocol; +@class SPDYStream; + +@protocol SPDYStreamDataDelegate +- (void)streamDataAvailable:(SPDYStream *)stream; +- (void)streamFinished:(SPDYStream *)stream; +@end + +@interface SPDYStream : NSObject +@property (nonatomic, weak) id client; +@property (nonatomic, weak) id dataDelegate; +@property (nonatomic) NSData *data; +@property (nonatomic) NSInputStream *dataStream; +@property (nonatomic, weak) NSURLRequest *request; +@property (nonatomic, weak) SPDYProtocol *protocol; +@property (nonatomic) SPDYStreamId streamId; +@property (nonatomic) uint8_t priority; +@property (nonatomic) bool local; +@property (nonatomic) bool localSideClosed; +@property (nonatomic) bool remoteSideClosed; +@property (nonatomic, readonly) bool closed; +@property (nonatomic) bool receivedReply; +@property (nonatomic, readonly) bool hasDataAvailable; +@property (nonatomic, readonly) bool hasDataPending; +@property (nonatomic) NSUInteger sendWindowSize; +@property (nonatomic) NSUInteger receiveWindowSize; +@property (nonatomic) NSUInteger sendWindowSizeLowerBound; +@property (nonatomic) NSUInteger receiveWindowSizeLowerBound; + +- (id)initWithProtocol:(SPDYProtocol *)protocol dataDelegate:(id)delegate; +- (void)startWithStreamId:(SPDYStreamId)id sendWindowSize:(NSUInteger)sendWindowSize receiveWindowSize:(NSUInteger)receiveWindowSize; +- (NSData *)readData:(NSUInteger)length error:(NSError **)pError; +- (void)closeWithError:(NSError *)error; +- (void)closeWithStatus:(SPDYStreamStatus)status; +- (void)didReceiveResponse:(NSDictionary *)headers; +- (void)didLoadData:(NSData *)data; +@end diff --git a/SPDY/SPDYStream.m b/SPDY/SPDYStream.m new file mode 100644 index 0000000..eac73f1 --- /dev/null +++ b/SPDY/SPDYStream.m @@ -0,0 +1,488 @@ +// +// SPDYStream.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import +#import "NSURLRequest+SPDYURLRequest.h" +#import "SPDYCommonLogger.h" +#import "SPDYError.h" +#import "SPDYProtocol.h" +#import "SPDYStream.h" + +#define INCLUDE_SPDY_RESPONSE_HEADERS 1 +#define DECOMPRESSED_CHUNK_LENGTH 8192 +#define USE_CFSTREAM 0 + +#if USE_CFSTREAM +#define SCHEDULE_STREAM() [self _scheduleCFReadStream] +#define UNSCHEDULE_STREAM() [self _unscheduleCFReadStream] +#else +#define SCHEDULE_STREAM() [self _scheduleNSInputStream] +#define UNSCHEDULE_STREAM() [self _unscheduleNSInputStream] +#endif + +@interface SPDYStream () +- (void)_scheduleCFReadStream; +- (void)_unscheduleCFReadStream; +- (void)_scheduleNSInputStream; +- (void)_unscheduleNSInputStream; +@end + +@implementation SPDYStream +{ + NSData *_data; + NSString *_dataFile; + NSInputStream *_dataStream; + NSRunLoop *_runLoop; + CFReadStreamRef _dataStreamRef; + CFRunLoopRef _runLoopRef; + NSUInteger _writeDataIndex; + z_stream _zlibStream; + bool _compressedResponse; + bool _writeStreamOpened; + int _zlibStreamStatus; +} + +- (id)init +{ + self = [super init]; + if (self) { + _localSideClosed = NO; + _remoteSideClosed = NO; + _compressedResponse = NO; + } + return self; +} + +- (id)initWithProtocol:(SPDYProtocol *)protocol dataDelegate:(id)delegate +{ + self = [super init]; + if (self) { + _protocol = protocol; + _client = protocol.client; + _request = protocol.request; + _priority = MIN((uint8_t)_request.SPDYPriority, 0x07); + _local = YES; + _localSideClosed = NO; + _remoteSideClosed = NO; + _receivedReply = NO; + _dataDelegate = delegate; + } + return self; +} + +- (void)startWithStreamId:(SPDYStreamId)streamId sendWindowSize:(NSUInteger)sendWindowSize receiveWindowSize:(NSUInteger)receiveWindowSize +{ + _streamId = streamId; + _sendWindowSize = sendWindowSize; + _receiveWindowSize = receiveWindowSize; + _sendWindowSizeLowerBound = 0; + _receiveWindowSizeLowerBound = 0; + _writeDataIndex = 0; + + if (_request.HTTPBody) { + _data = _request.HTTPBody; + } else if (_request.SPDYBodyFile) { + _dataStream = [[NSInputStream alloc] initWithFileAtPath:_request.SPDYBodyFile]; + } else if (_request.HTTPBodyStream) { + SPDY_WARNING(@"using HTTPBodyStream on a SPDY request is subject to a potentially fatal CFNetwork bug"); + _dataStream = _request.HTTPBodyStream; + } else if (_request.SPDYBodyStream) { + SPDY_WARNING(@"using SPDYBodyStream may fail for redirected requests or requests that meet authentication challenges"); + _dataStream = _request.SPDYBodyStream; + } + + if (_dataStream) { + _dataStreamRef = (__bridge CFReadStreamRef)_dataStream; + SCHEDULE_STREAM(); + } +} + +- (void)setDataStream:(NSInputStream *)dataStream +{ + _dataStream = dataStream; + _dataStreamRef = (__bridge CFReadStreamRef)dataStream; +} + +- (void)dealloc +{ + if (_compressedResponse) { + inflateEnd(&_zlibStream); + } + + if (_dataStream && (_runLoop || _runLoopRef)) { + UNSCHEDULE_STREAM(); + } +} + +- (void)setProtocol:(SPDYProtocol *)protocol +{ + _protocol = protocol; + _client = protocol.client; +} + +- (void)closeWithError:(NSError *)error +{ + if (_client) { + // Failing to pass an error here leads to null pointer exception + if (!error) { + error = SPDY_SOCKET_ERROR(SPDYSocketTransportError, @"Unknown socket error."); + } + [_client URLProtocol:_protocol didFailWithError:error]; + } +} + +- (void)closeWithStatus:(SPDYStreamStatus)status +{ + if (_client) { + NSError *error = SPDY_STREAM_ERROR((SPDYStreamError)status, @"SPDY stream closed."); + [_client URLProtocol:_protocol didFailWithError:error]; + } +} + +- (void)setLocalSideClosed:(bool)localSideClosed +{ + _localSideClosed = localSideClosed; + if (_localSideClosed && _remoteSideClosed && _client) { + [_client URLProtocolDidFinishLoading:_protocol]; + } +} + +- (void)setRemoteSideClosed:(bool)remoteSideClosed +{ + _remoteSideClosed = remoteSideClosed; + if (_localSideClosed && _remoteSideClosed && _client) { + [_client URLProtocolDidFinishLoading:_protocol]; + } +} + +- (bool)closed +{ + return _localSideClosed && _remoteSideClosed; +} + +- (bool)hasDataAvailable +{ + bool writeStreamAvailable = ( + _dataStream && + _writeStreamOpened && + _dataStream.streamStatus == NSStreamStatusOpen && + _dataStream.hasBytesAvailable + ); + + return (_data && _data.length - _writeDataIndex > 0) || + (writeStreamAvailable); +} + +- (bool)hasDataPending +{ + bool writeStreamPending = ( + _dataStream && + (!_writeStreamOpened || _dataStream.streamStatus < NSStreamStatusAtEnd) + ); + + return (_data && _data.length - _writeDataIndex > 0) || + (writeStreamPending); +} + +- (NSData *)readData:(NSUInteger)length error:(NSError **)pError +{ + if (_dataStream) { + if (length > 0 && _dataStream.hasBytesAvailable) { + NSMutableData *writeData = [[NSMutableData alloc] initWithLength:length]; + NSInteger bytesRead = [_dataStream read:(uint8_t *)writeData.bytes maxLength:length]; + + if (bytesRead > 0) { + writeData.length = (NSUInteger)bytesRead; + return writeData; + } else if (bytesRead < 0) { + SPDY_DEBUG(@"SPDY stream read error"); + if (pError) { + NSDictionary *info = @{ NSLocalizedDescriptionKey: @"Unable to read request body stream" }; + *pError = [[NSError alloc] initWithDomain:SPDYStreamErrorDomain + code:SPDYStreamCancel + userInfo:info]; + } + _data = nil; + } + } + } else if (_data) { + length = MAX(MIN(_data.length - _writeDataIndex, length), 0); + if (length == 0) return nil; + + uint8_t *bytes = ((uint8_t *)_data.bytes + _writeDataIndex); + NSData *writeData = [[NSData alloc] initWithBytesNoCopy:bytes length:length freeWhenDone:NO]; + _writeDataIndex += length; + + return writeData; + } + + return nil; +} + +- (void)didReceiveResponse:(NSDictionary *)headers +{ + _receivedReply = YES; + + NSInteger statusCode = [headers[@":status"] intValue]; + if (statusCode < 100 || statusCode > 599) { + NSDictionary *info = @{ NSLocalizedDescriptionKey: @"invalid http response code" }; + NSError *error = [[NSError alloc] initWithDomain:NSURLErrorDomain + code:NSURLErrorBadServerResponse + userInfo:info]; + [_client URLProtocol:_protocol didFailWithError:error]; + return; + } + + NSString *version = headers[@":version"]; + if (!version) { + NSDictionary *info = @{ NSLocalizedDescriptionKey: @"response missing version header" }; + NSError *error = [[NSError alloc] initWithDomain:NSURLErrorDomain + code:NSURLErrorBadServerResponse + userInfo:info]; + [_client URLProtocol:_protocol didFailWithError:error]; + return; + } + + NSMutableDictionary *allHTTPHeaders = [[NSMutableDictionary alloc] init]; + for (NSString *key in headers) { + if (![key hasPrefix:@":"]) { + id headerValue = headers[key]; + if ([headerValue isKindOfClass:NSClassFromString(@"NSArray")]) { + allHTTPHeaders[key] = [headers[key] componentsJoinedByString:@", "]; + } else { + allHTTPHeaders[key] = headers[key]; + } + } + } + + NSURL *requestURL = _protocol.request.URL; + + if (_protocol.request.HTTPShouldHandleCookies) { + NSString *httpSetCookie = allHTTPHeaders[@"set-cookie"]; + if (httpSetCookie) { + // HTTP header field names are supposed to be case-insensitive, but + // NSHTTPCookie will fail to automatically parse cookies unless we + // force the case-senstive name "Set-Cookie" + NSDictionary *cookieHeaders = @{ @"Set-Cookie": httpSetCookie }; + [allHTTPHeaders removeObjectForKey:@"set-cookie"]; + NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:cookieHeaders + forURL:requestURL]; + + [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies:cookies + forURL:requestURL + mainDocumentURL:_protocol.request.mainDocumentURL]; + } + } + +#if INCLUDE_SPDY_RESPONSE_HEADERS + allHTTPHeaders[@"x-spdy-version"] = @"3"; + allHTTPHeaders[@"x-spdy-stream-id"] = [@(_streamId) stringValue]; +#endif + + NSString *encoding = allHTTPHeaders[@"content-encoding"]; + _compressedResponse = [encoding hasPrefix:@"deflate"] || [encoding hasPrefix:@"gzip"]; + if (_compressedResponse) { + bzero(&_zlibStream, sizeof(_zlibStream)); + _zlibStreamStatus = inflateInit2(&_zlibStream, MAX_WBITS + 32); + } + + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:requestURL + statusCode:statusCode + HTTPVersion:version + headerFields:allHTTPHeaders]; + + NSString *location = allHTTPHeaders[@"location"]; + + if (location != nil) { + NSURL *redirectURL = [[NSURL alloc] initWithString:location relativeToURL:requestURL]; + if (redirectURL == nil) { + NSError *error = [[NSError alloc] initWithDomain:NSURLErrorDomain + code:NSURLErrorRedirectToNonExistentLocation + userInfo:nil]; + [_client URLProtocol:_protocol didFailWithError:error]; + return; + } + + NSMutableURLRequest *redirect = [_protocol.request mutableCopy]; + redirect.URL = redirectURL; + redirect.SPDYPriority = _request.SPDYPriority; + redirect.SPDYBodyFile = _request.SPDYBodyFile; + + if (statusCode == 303) { + redirect.HTTPMethod = @"GET"; + } + + [_client URLProtocol:_protocol wasRedirectedToRequest:redirect redirectResponse:response]; + return; + } + + [_client URLProtocol:_protocol + didReceiveResponse:response + cacheStoragePolicy:NSURLCacheStorageAllowed]; +} + +- (void)didLoadData:(NSData *)data +{ + if (_compressedResponse && data.length > 0) { + _zlibStream.avail_in = (uInt)data.length; + _zlibStream.next_in = (uint8_t *)data.bytes; + + while (_zlibStreamStatus == Z_OK && (_zlibStream.avail_in > 0 || _zlibStream.avail_out == 0)) { + uint8_t *inflatedBytes = malloc(sizeof(uint8_t) * DECOMPRESSED_CHUNK_LENGTH); + if (inflatedBytes == NULL) { + SPDY_ERROR(@"error decompressing response data: malloc failed"); + NSError *error = [[NSError alloc] initWithDomain:NSURLErrorDomain + code:NSURLErrorCannotDecodeContentData + userInfo:nil]; + [_client URLProtocol:_protocol didFailWithError:error]; + return; + } + + _zlibStream.avail_out = DECOMPRESSED_CHUNK_LENGTH; + _zlibStream.next_out = inflatedBytes; + _zlibStreamStatus = inflate(&_zlibStream, Z_SYNC_FLUSH); + + NSMutableData *inflatedData = [[NSMutableData alloc] initWithBytesNoCopy:inflatedBytes length:DECOMPRESSED_CHUNK_LENGTH freeWhenDone:YES]; + inflatedData.length = DECOMPRESSED_CHUNK_LENGTH - _zlibStream.avail_out; + [_client URLProtocol:_protocol didLoadData:inflatedData]; + + // This can happen if the decompressed data is size N * DECOMPRESSED_CHUNK_LENGTH, + // in which case we had to make an additional call to inflate() despite there being + // no more input to ensure there wasn't any pending output in the zlib stream. + if (_zlibStreamStatus == Z_BUF_ERROR) { + _zlibStreamStatus = Z_OK; + break; + } + } + + if (_zlibStreamStatus != Z_OK && _zlibStreamStatus != Z_STREAM_END) { + SPDY_WARNING(@"error decompressing response data: bad z_stream state"); + NSError *error = [[NSError alloc] initWithDomain:NSURLErrorDomain + code:NSURLErrorCannotDecodeContentData + userInfo:nil]; + [_client URLProtocol:_protocol didFailWithError:error]; + } + } else { + [_client URLProtocol:_protocol didLoadData:[data copy]]; + } +} + +#pragma mark CFReadStreamClient + +static void SPDYStreamCFReadStreamCallback(CFReadStreamRef stream, CFStreamEventType type, void *pStream) +{ + @autoreleasepool { + SPDYStream * volatile spdyStream = (__bridge SPDYStream*)pStream; + [spdyStream handleDataStreamEvent:type]; + } +} + +- (void)handleDataStreamEvent:(CFStreamEventType)eventType +{ + if (eventType & kCFStreamEventOpenCompleted) { + _writeStreamOpened = YES; + } else if (!_writeStreamOpened) { + return; + } + + CFOptionFlags closeEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; + if (eventType & closeEvents) { + [_dataDelegate streamFinished:self]; + } else { + [_dataDelegate streamDataAvailable:self]; + } +} + +#pragma mark NSStreamDelegate + +- (void)stream:(NSStream *)aStream handleEvent:(NSStreamEvent)eventCode +{ + if (eventCode & NSStreamEventOpenCompleted) { + _writeStreamOpened = YES; + } else if (!_writeStreamOpened) { + return; + } + + if (_dataStream.streamStatus >= NSStreamStatusAtEnd) { + [_dataDelegate streamFinished:self]; + } else { + [_dataDelegate streamDataAvailable:self]; + } +} + +#pragma mark private methods +- (void)_scheduleCFReadStream +{ + SPDY_DEBUG(@"scheduling CFReadStream: %p", _dataStreamRef); + _runLoopRef = CFRunLoopGetCurrent(); + + CFStreamClientContext clientContext; + clientContext.version = 0; + clientContext.info = (__bridge void *)(self); + clientContext.retain = nil; + clientContext.release = nil; + clientContext.copyDescription = nil; + + CFOptionFlags readStreamEvents = + kCFStreamEventHasBytesAvailable | + kCFStreamEventErrorOccurred | + kCFStreamEventEndEncountered | + kCFStreamEventOpenCompleted; + + CFReadStreamClientCallBack clientCallback = + (CFReadStreamClientCallBack)&SPDYStreamCFReadStreamCallback; + + if (!CFReadStreamSetClient(_dataStreamRef, + readStreamEvents, + clientCallback, + &clientContext)) + { + SPDY_ERROR(@"couldn't attach read stream to runloop"); + return; + } + + CFReadStreamScheduleWithRunLoop(_dataStreamRef, _runLoopRef, kCFRunLoopDefaultMode); + if (!CFReadStreamOpen(_dataStreamRef)) { + SPDY_ERROR(@"can't open stream: %@", _dataStreamRef); + return; + } +} + +- (void)_scheduleNSInputStream +{ + SPDY_DEBUG(@"scheduling NSInputStream: %@", _dataStream); + _dataStream.delegate = self; + _runLoop = [NSRunLoop currentRunLoop]; + [_dataStream scheduleInRunLoop:_runLoop forMode:NSDefaultRunLoopMode]; + [_dataStream open]; +} + +- (void)_unscheduleCFReadStream +{ + SPDY_DEBUG(@"unscheduling CFReadStream: %p", _dataStreamRef); + CFReadStreamClose(_dataStreamRef); + CFReadStreamSetClient(_dataStreamRef, kCFStreamEventNone, NULL, NULL); + _runLoopRef = NULL; +} + +- (void)_unscheduleNSInputStream +{ + SPDY_DEBUG(@"unscheduling NSInputStream: %@", _dataStream); + [_dataStream close]; + _dataStream.delegate = nil; + _runLoop = nil; +} + +@end diff --git a/SPDY/SPDYStreamManager.h b/SPDY/SPDYStreamManager.h new file mode 100644 index 0000000..f52ad95 --- /dev/null +++ b/SPDY/SPDYStreamManager.h @@ -0,0 +1,35 @@ +// +// SPDYStreamManager.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import +#import "SPDYDefinitions.h" + +@class SPDYProtocol; +@class SPDYStream; + +/** + Data structure for management of SPDYStreams. +*/ +@interface SPDYStreamManager : NSObject + +@property (nonatomic, readonly) NSUInteger count; +@property (nonatomic, readonly) NSUInteger localCount; +@property (nonatomic, readonly) NSUInteger remoteCount; +- (void)addStream:(SPDYStream *)stream; +- (id)objectAtIndexedSubscript:(NSUInteger)idx; +- (id)objectForKeyedSubscript:(id)key; +- (SPDYStream *)nextPriorityStream; +- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx; +- (void)removeStreamWithStreamId:(SPDYStreamId)streamId; +- (void)removeStreamForProtocol:(SPDYProtocol *)protocol; +- (void)removeAllStreams; + +@end diff --git a/SPDY/SPDYStreamManager.m b/SPDY/SPDYStreamManager.m new file mode 100644 index 0000000..7d5f5fa --- /dev/null +++ b/SPDY/SPDYStreamManager.m @@ -0,0 +1,266 @@ +// +// SPDYStreamManager.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import +#import "SPDYStreamManager.h" +#import "SPDYProtocol.h" +#import "SPDYStream.h" + + +@interface SPDYStreamNode : NSObject +@end + +@implementation SPDYStreamNode +{ + @public + __strong SPDYStreamNode *next; + __strong SPDYStreamNode *prev; + __strong SPDYStream *stream; + __unsafe_unretained SPDYProtocol *protocol; + SPDYStreamId streamId; +} +@end + +@interface SPDYStreamManager () +- (void)_removeListNode:(SPDYStreamNode *)node; +@end + +@implementation SPDYStreamManager +{ + SPDYStreamNode *_priorityHead[8]; + SPDYStreamNode *_priorityLast[8]; + CFMutableDictionaryRef _nodesByStreamId; + CFMutableDictionaryRef _nodesByProtocol; + NSUInteger _localCount; + NSUInteger _remoteCount; + unsigned long _mutations; +} + +Boolean SPDYStreamIdEqual(const void *key1, const void *key2) { + return (SPDYStreamId)key1 == (SPDYStreamId)key2; +} + +CFHashCode SPDYStreamIdHash(const void *key) { + return (CFHashCode)((SPDYStreamId)key); +} + +CFStringRef SPDYStreamIdCopyDescription(const void *key) { + return CFStringCreateWithFormat(kCFAllocatorDefault, NULL, CFSTR("%d"), (SPDYStreamId)key); +} + +- (id)init +{ + NSAssert(sizeof(void *) <= sizeof(unsigned long), @"pointer width must be <= unsigned long width"); + NSAssert(sizeof(void *) >= sizeof(SPDYStreamId), @"pointer width must be >= SPDYStreamId width"); + + self = [super init]; + if (self) { + CFDictionaryKeyCallBacks SPDYStreamIdKeyCallbacks = { + 0, NULL, NULL, + SPDYStreamIdCopyDescription, + SPDYStreamIdEqual, + SPDYStreamIdHash + }; + _nodesByStreamId = CFDictionaryCreateMutable( + kCFAllocatorDefault, 100, + &SPDYStreamIdKeyCallbacks, + &kCFTypeDictionaryValueCallBacks + ); + _nodesByProtocol = CFDictionaryCreateMutable( + kCFAllocatorDefault, 100, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks + ); + _localCount = 0; + _remoteCount = 0; + _mutations = 0; + } + return self; +} + +- (void)dealloc +{ + CFRelease(_nodesByStreamId); + CFRelease(_nodesByProtocol); +} + +- (NSUInteger)count +{ + return _localCount + _remoteCount; +} + +- (id)objectAtIndexedSubscript:(NSUInteger)idx +{ + SPDYStreamNode *node = (id)CFDictionaryGetValue(_nodesByStreamId, (void *)idx); + return node ? node->stream : nil; +} + +- (id)objectForKeyedSubscript:(id)key +{ + SPDYStreamNode *node = (id)CFDictionaryGetValue(_nodesByProtocol, (__bridge CFTypeRef)key); + return node ? node->stream : nil; +} + +- (SPDYStream *)nextPriorityStream +{ + SPDYStreamNode *currentNode; + for (int priority = 0; priority < 8 && currentNode == NULL; priority++) { + currentNode = _priorityHead[priority]; + } + + if (currentNode) { + return currentNode->stream; + } + + return nil; +} + +- (void)addStream:(SPDYStream *)stream +{ + SPDYStreamNode *node = [[SPDYStreamNode alloc] init]; + node->stream = stream; + node->protocol = stream.protocol; + node->streamId = stream.streamId; + [self _addListNode:node]; +} + +- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx +{ + if (obj) { + SPDYStreamNode *node = [[SPDYStreamNode alloc] init]; + SPDYStream *stream = obj; + node->stream = stream; + node->protocol = stream.protocol; + node->streamId = (SPDYStreamId)idx; + + [self _addListNode:node]; + } else { + [self removeStreamWithStreamId:(SPDYStreamId)idx]; + } +} + +- (void)_addListNode:(SPDYStreamNode *)node +{ + // Update linked list + uint8_t priority = node->stream.priority; + if (_priorityHead[priority] == NULL) { + _priorityHead[priority] = node; + _priorityLast[priority] = node; + } else { + _priorityLast[priority]->next = node; + node->prev = _priorityLast[priority]; + _priorityLast[priority] = node; + } + + // Update hash maps + NSAssert(node->streamId != 0 || node->protocol != nil, @"cannot insert unaddressable stream"); + NSAssert(CFDictionaryGetValue(_nodesByStreamId, (void *)(uintptr_t)node->streamId) == NULL, @"cannot insert stream with duplicate streamId"); + if (node->streamId) { + CFDictionarySetValue(_nodesByStreamId, (void *)(uintptr_t)node->streamId, (__bridge CFTypeRef)node); + } + if (node->protocol) { + CFDictionarySetValue(_nodesByProtocol, (__bridge CFTypeRef)node->protocol, (__bridge CFTypeRef)node); + } + + // Update counts + if (node->stream.local) { + _localCount += 1; + } else { + _remoteCount += 1; + } + + _mutations += 1; +} + +- (void)removeStreamWithStreamId:(SPDYStreamId)streamId +{ + SPDYStreamNode *node = (id)CFDictionaryGetValue(_nodesByStreamId, (void *)(uintptr_t)streamId); + if (node) [self _removeListNode:node]; +} + +- (void)removeStreamForProtocol:(SPDYProtocol *)protocol +{ + SPDYStreamNode *node = (id)CFDictionaryGetValue(_nodesByProtocol, (__bridge CFTypeRef)protocol); + if (node) [self _removeListNode:node]; +} + +- (void)_removeListNode:(SPDYStreamNode *)node +{ + // Update linked list + uint8_t priority = node->stream.priority; + if (node->next != NULL) node->next->prev = node->prev; + if (node->prev != NULL) node->prev->next = node->next; + if (_priorityHead[priority] == node) _priorityHead[priority] = node->next; + if (_priorityLast[priority] == node) _priorityLast[priority] = node->prev; + + // Update hash maps + if (node->streamId) { + CFDictionaryRemoveValue(_nodesByStreamId, (void *)(uintptr_t)node->streamId); + } + if (node->protocol) { + CFDictionaryRemoveValue(_nodesByProtocol, (__bridge CFTypeRef)node->protocol); + } + + // Update counts + if (node->stream.local) { + _localCount -= 1; + } else { + _remoteCount -= 1; + } + + _mutations += 1; +} + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])buffer count:(NSUInteger)len +{ + SPDYStreamNode *currentNode = (__bridge SPDYStreamNode *)((void *)state->extra[0]); + unsigned long *priority = &(state->state); + + for (; *priority < 8 && currentNode == NULL; *priority += 1) { + currentNode = _priorityHead[*priority]; + } + + if (currentNode == NULL) { + return 0; + } + + NSUInteger i; + for (i = 0; i < len && currentNode != NULL; i++) { + buffer[i] = currentNode->stream; + currentNode = currentNode->next; + } + + state->extra[0] = (unsigned long)currentNode; + state->itemsPtr = buffer; + state->mutationsPtr = &_mutations; + return i; +} + +- (void)removeAllStreams +{ + // Update linked list + for (int i = 0; i < 8; i++) { + _priorityHead[i] = NULL; + _priorityLast[i] = NULL; + } + + // Update hash maps + CFDictionaryRemoveAllValues(_nodesByStreamId); + CFDictionaryRemoveAllValues(_nodesByProtocol); + + // Update counts + _localCount = 0; + _remoteCount = 0; + + _mutations += 1; +} + +@end diff --git a/SPDY/SPDYTLSTrustEvaluator.h b/SPDY/SPDYTLSTrustEvaluator.h new file mode 100644 index 0000000..078d03a --- /dev/null +++ b/SPDY/SPDYTLSTrustEvaluator.h @@ -0,0 +1,16 @@ +// +// SPDYTLSTrustEvaluator.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import + +@protocol SPDYTLSTrustEvaluator +- (BOOL)evaluateServerTrust:(SecTrustRef)trust forHost:(NSString *)host; +@end diff --git a/SPDY/SPDYZLibCommon.h b/SPDY/SPDYZLibCommon.h new file mode 100644 index 0000000..00dc45c --- /dev/null +++ b/SPDY/SPDYZLibCommon.h @@ -0,0 +1,194 @@ +// +// SPDYZLibCommon.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import + +// ZLib Dictionary +static const uint8_t kSPDYDict[] = { + 0x00, 0x00, 0x00, 0x07, 0x6f, 0x70, 0x74, 0x69, // - - - - o p t i + 0x6f, 0x6e, 0x73, 0x00, 0x00, 0x00, 0x04, 0x68, // o n s - - - - h + 0x65, 0x61, 0x64, 0x00, 0x00, 0x00, 0x04, 0x70, // e a d - - - - p + 0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x03, 0x70, // o s t - - - - p + 0x75, 0x74, 0x00, 0x00, 0x00, 0x06, 0x64, 0x65, // u t - - - - d e + 0x6c, 0x65, 0x74, 0x65, 0x00, 0x00, 0x00, 0x05, // l e t e - - - - + 0x74, 0x72, 0x61, 0x63, 0x65, 0x00, 0x00, 0x00, // t r a c e - - - + 0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x00, // - a c c e p t - + 0x00, 0x00, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, // - - - a c c e p + 0x74, 0x2d, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // t - c h a r s e + 0x74, 0x00, 0x00, 0x00, 0x0f, 0x61, 0x63, 0x63, // t - - - - a c c + 0x65, 0x70, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // e p t - e n c o + 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x0f, // d i n g - - - - + 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c, // a c c e p t - l + 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x00, // a n g u a g e - + 0x00, 0x00, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, // - - - a c c e p + 0x74, 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, // t - r a n g e s + 0x00, 0x00, 0x00, 0x03, 0x61, 0x67, 0x65, 0x00, // - - - - a g e - + 0x00, 0x00, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, // - - - a l l o w + 0x00, 0x00, 0x00, 0x0d, 0x61, 0x75, 0x74, 0x68, // - - - - a u t h + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, // o r i z a t i o + 0x6e, 0x00, 0x00, 0x00, 0x0d, 0x63, 0x61, 0x63, // n - - - - c a c + 0x68, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, // h e - c o n t r + 0x6f, 0x6c, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x6f, // o l - - - - c o + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, // n n e c t i o n + 0x00, 0x00, 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t + 0x65, 0x6e, 0x74, 0x2d, 0x62, 0x61, 0x73, 0x65, // e n t - b a s e + 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t + 0x65, 0x6e, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // e n t - e n c o + 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, // d i n g - - - - + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, // c o n t e n t - + 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, // l a n g u a g e + 0x00, 0x00, 0x00, 0x0e, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t + 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67, // e n t - l e n g + 0x74, 0x68, 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, // t h - - - - c o + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x6f, // n t e n t - l o + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, // c a t i o n - - + 0x00, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // - - c o n t e n + 0x74, 0x2d, 0x6d, 0x64, 0x35, 0x00, 0x00, 0x00, // t - m d 5 - - - + 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, // - c o n t e n t + 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, // - r a n g e - - + 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // - - c o n t e n + 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x00, 0x00, // t - t y p e - - + 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x00, 0x00, // - - d a t e - - + 0x00, 0x04, 0x65, 0x74, 0x61, 0x67, 0x00, 0x00, // - - e t a g - - + 0x00, 0x06, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, // - - e x p e c t + 0x00, 0x00, 0x00, 0x07, 0x65, 0x78, 0x70, 0x69, // - - - - e x p i + 0x72, 0x65, 0x73, 0x00, 0x00, 0x00, 0x04, 0x66, // r e s - - - - f + 0x72, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x04, 0x68, // r o m - - - - h + 0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x08, 0x69, // o s t - - - - i + 0x66, 0x2d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, // f - m a t c h - + 0x00, 0x00, 0x11, 0x69, 0x66, 0x2d, 0x6d, 0x6f, // - - - i f - m o + 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2d, 0x73, // d i f i e d - s + 0x69, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x0d, // i n c e - - - - + 0x69, 0x66, 0x2d, 0x6e, 0x6f, 0x6e, 0x65, 0x2d, // i f - n o n e - + 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, 0x00, 0x00, // m a t c h - - - + 0x08, 0x69, 0x66, 0x2d, 0x72, 0x61, 0x6e, 0x67, // - i f - r a n g + 0x65, 0x00, 0x00, 0x00, 0x13, 0x69, 0x66, 0x2d, // e - - - - i f - + 0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, // u n m o d i f i + 0x65, 0x64, 0x2d, 0x73, 0x69, 0x6e, 0x63, 0x65, // e d - s i n c e + 0x00, 0x00, 0x00, 0x0d, 0x6c, 0x61, 0x73, 0x74, // - - - - l a s t + 0x2d, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, // - m o d i f i e + 0x64, 0x00, 0x00, 0x00, 0x08, 0x6c, 0x6f, 0x63, // d - - - - l o c + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, // a t i o n - - - + 0x0c, 0x6d, 0x61, 0x78, 0x2d, 0x66, 0x6f, 0x72, // - m a x - f o r + 0x77, 0x61, 0x72, 0x64, 0x73, 0x00, 0x00, 0x00, // w a r d s - - - + 0x06, 0x70, 0x72, 0x61, 0x67, 0x6d, 0x61, 0x00, // - p r a g m a - + 0x00, 0x00, 0x12, 0x70, 0x72, 0x6f, 0x78, 0x79, // - - - p r o x y + 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, // - a u t h e n t + 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, 0x00, // i c a t e - - - + 0x13, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2d, 0x61, // - p r o x y - a + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, // u t h o r i z a + 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05, // t i o n - - - - + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, 0x00, // r a n g e - - - + 0x07, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x72, // - r e f e r e r + 0x00, 0x00, 0x00, 0x0b, 0x72, 0x65, 0x74, 0x72, // - - - - r e t r + 0x79, 0x2d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x00, // y - a f t e r - + 0x00, 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, // - - - s e r v e + 0x72, 0x00, 0x00, 0x00, 0x02, 0x74, 0x65, 0x00, // r - - - - t e - + 0x00, 0x00, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, // - - - t r a i l + 0x65, 0x72, 0x00, 0x00, 0x00, 0x11, 0x74, 0x72, // e r - - - - t r + 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x2d, 0x65, // a n s f e r - e + 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x00, // n c o d i n g - + 0x00, 0x00, 0x07, 0x75, 0x70, 0x67, 0x72, 0x61, // - - - u p g r a + 0x64, 0x65, 0x00, 0x00, 0x00, 0x0a, 0x75, 0x73, // d e - - - - u s + 0x65, 0x72, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, // e r - a g e n t + 0x00, 0x00, 0x00, 0x04, 0x76, 0x61, 0x72, 0x79, // - - - - v a r y + 0x00, 0x00, 0x00, 0x03, 0x76, 0x69, 0x61, 0x00, // - - - - v i a - + 0x00, 0x00, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, // - - - w a r n i + 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, 0x77, 0x77, // n g - - - - w w + 0x77, 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, // w - a u t h e n + 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, // t i c a t e - - + 0x00, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, // - - m e t h o d + 0x00, 0x00, 0x00, 0x03, 0x67, 0x65, 0x74, 0x00, // - - - - g e t - + 0x00, 0x00, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, // - - - s t a t u + 0x73, 0x00, 0x00, 0x00, 0x06, 0x32, 0x30, 0x30, // s - - - - 2 0 0 + 0x20, 0x4f, 0x4b, 0x00, 0x00, 0x00, 0x07, 0x76, // - O K - - - - v + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x00, // e r s i o n - - + 0x00, 0x08, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, // - - H T T P - 1 + 0x2e, 0x31, 0x00, 0x00, 0x00, 0x03, 0x75, 0x72, // - 1 - - - - u r + 0x6c, 0x00, 0x00, 0x00, 0x06, 0x70, 0x75, 0x62, // l - - - - p u b + 0x6c, 0x69, 0x63, 0x00, 0x00, 0x00, 0x0a, 0x73, // l i c - - - - s + 0x65, 0x74, 0x2d, 0x63, 0x6f, 0x6f, 0x6b, 0x69, // e t - c o o k i + 0x65, 0x00, 0x00, 0x00, 0x0a, 0x6b, 0x65, 0x65, // e - - - - k e e + 0x70, 0x2d, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x00, // p - a l i v e - + 0x00, 0x00, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, // - - - o r i g i + 0x6e, 0x31, 0x30, 0x30, 0x31, 0x30, 0x31, 0x32, // n 1 0 0 1 0 1 2 + 0x30, 0x31, 0x32, 0x30, 0x32, 0x32, 0x30, 0x35, // 0 1 2 0 2 2 0 5 + 0x32, 0x30, 0x36, 0x33, 0x30, 0x30, 0x33, 0x30, // 2 0 6 3 0 0 3 0 + 0x32, 0x33, 0x30, 0x33, 0x33, 0x30, 0x34, 0x33, // 2 3 0 3 3 0 4 3 + 0x30, 0x35, 0x33, 0x30, 0x36, 0x33, 0x30, 0x37, // 0 5 3 0 6 3 0 7 + 0x34, 0x30, 0x32, 0x34, 0x30, 0x35, 0x34, 0x30, // 4 0 2 4 0 5 4 0 + 0x36, 0x34, 0x30, 0x37, 0x34, 0x30, 0x38, 0x34, // 6 4 0 7 4 0 8 4 + 0x30, 0x39, 0x34, 0x31, 0x30, 0x34, 0x31, 0x31, // 0 9 4 1 0 4 1 1 + 0x34, 0x31, 0x32, 0x34, 0x31, 0x33, 0x34, 0x31, // 4 1 2 4 1 3 4 1 + 0x34, 0x34, 0x31, 0x35, 0x34, 0x31, 0x36, 0x34, // 4 4 1 5 4 1 6 4 + 0x31, 0x37, 0x35, 0x30, 0x32, 0x35, 0x30, 0x34, // 1 7 5 0 2 5 0 4 + 0x35, 0x30, 0x35, 0x32, 0x30, 0x33, 0x20, 0x4e, // 5 0 5 2 0 3 - N + 0x6f, 0x6e, 0x2d, 0x41, 0x75, 0x74, 0x68, 0x6f, // o n - A u t h o + 0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, // r i t a t i v e + 0x20, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, // - I n f o r m a + 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x30, 0x34, 0x20, // t i o n 2 0 4 - + 0x4e, 0x6f, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x65, // N o - C o n t e + 0x6e, 0x74, 0x33, 0x30, 0x31, 0x20, 0x4d, 0x6f, // n t 3 0 1 - M o + 0x76, 0x65, 0x64, 0x20, 0x50, 0x65, 0x72, 0x6d, // v e d - P e r m + 0x61, 0x6e, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x34, // a n e n t l y 4 + 0x30, 0x30, 0x20, 0x42, 0x61, 0x64, 0x20, 0x52, // 0 0 - B a d - R + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x34, 0x30, // e q u e s t 4 0 + 0x31, 0x20, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, // 1 - U n a u t h + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x34, 0x30, // o r i z e d 4 0 + 0x33, 0x20, 0x46, 0x6f, 0x72, 0x62, 0x69, 0x64, // 3 - F o r b i d + 0x64, 0x65, 0x6e, 0x34, 0x30, 0x34, 0x20, 0x4e, // d e n 4 0 4 - N + 0x6f, 0x74, 0x20, 0x46, 0x6f, 0x75, 0x6e, 0x64, // o t - F o u n d + 0x35, 0x30, 0x30, 0x20, 0x49, 0x6e, 0x74, 0x65, // 5 0 0 - I n t e + 0x72, 0x6e, 0x61, 0x6c, 0x20, 0x53, 0x65, 0x72, // r n a l - S e r + 0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6f, // v e r - E r r o + 0x72, 0x35, 0x30, 0x31, 0x20, 0x4e, 0x6f, 0x74, // r 5 0 1 - N o t + 0x20, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, // - I m p l e m e + 0x6e, 0x74, 0x65, 0x64, 0x35, 0x30, 0x33, 0x20, // n t e d 5 0 3 - + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, // S e r v i c e - + 0x55, 0x6e, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, // U n a v a i l a + 0x62, 0x6c, 0x65, 0x4a, 0x61, 0x6e, 0x20, 0x46, // b l e J a n - F + 0x65, 0x62, 0x20, 0x4d, 0x61, 0x72, 0x20, 0x41, // e b - M a r - A + 0x70, 0x72, 0x20, 0x4d, 0x61, 0x79, 0x20, 0x4a, // p r - M a y - J + 0x75, 0x6e, 0x20, 0x4a, 0x75, 0x6c, 0x20, 0x41, // u n - J u l - A + 0x75, 0x67, 0x20, 0x53, 0x65, 0x70, 0x74, 0x20, // u g - S e p t - + 0x4f, 0x63, 0x74, 0x20, 0x4e, 0x6f, 0x76, 0x20, // O c t - N o v - + 0x44, 0x65, 0x63, 0x20, 0x30, 0x30, 0x3a, 0x30, // D e c - 0 0 - 0 + 0x30, 0x3a, 0x30, 0x30, 0x20, 0x4d, 0x6f, 0x6e, // 0 - 0 0 - M o n + 0x2c, 0x20, 0x54, 0x75, 0x65, 0x2c, 0x20, 0x57, // - - T u e - - W + 0x65, 0x64, 0x2c, 0x20, 0x54, 0x68, 0x75, 0x2c, // e d - - T h u - + 0x20, 0x46, 0x72, 0x69, 0x2c, 0x20, 0x53, 0x61, // - F r i - - S a + 0x74, 0x2c, 0x20, 0x53, 0x75, 0x6e, 0x2c, 0x20, // t - - S u n - - + 0x47, 0x4d, 0x54, 0x63, 0x68, 0x75, 0x6e, 0x6b, // G M T c h u n k + 0x65, 0x64, 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, // e d - t e x t - + 0x68, 0x74, 0x6d, 0x6c, 0x2c, 0x69, 0x6d, 0x61, // h t m l - i m a + 0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x2c, 0x69, // g e - p n g - i + 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x6a, 0x70, 0x67, // m a g e - j p g + 0x2c, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x67, // - i m a g e - g + 0x69, 0x66, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // i f - a p p l i + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // c a t i o n - x + 0x6d, 0x6c, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // m l - a p p l i + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // c a t i o n - x + 0x68, 0x74, 0x6d, 0x6c, 0x2b, 0x78, 0x6d, 0x6c, // h t m l - x m l + 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, // - t e x t - p l + 0x61, 0x69, 0x6e, 0x2c, 0x74, 0x65, 0x78, 0x74, // a i n - t e x t + 0x2f, 0x6a, 0x61, 0x76, 0x61, 0x73, 0x63, 0x72, // - j a v a s c r + 0x69, 0x70, 0x74, 0x2c, 0x70, 0x75, 0x62, 0x6c, // i p t - p u b l + 0x69, 0x63, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, // i c p r i v a t + 0x65, 0x6d, 0x61, 0x78, 0x2d, 0x61, 0x67, 0x65, // e m a x - a g e + 0x3d, 0x67, 0x7a, 0x69, 0x70, 0x2c, 0x64, 0x65, // - g z i p - d e + 0x66, 0x6c, 0x61, 0x74, 0x65, 0x2c, 0x73, 0x64, // f l a t e - s d + 0x63, 0x68, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // c h c h a r s e + 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38, 0x63, // t - u t f - 8 c + 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x69, // h a r s e t - i + 0x73, 0x6f, 0x2d, 0x38, 0x38, 0x35, 0x39, 0x2d, // s o - 8 8 5 9 - + 0x31, 0x2c, 0x75, 0x74, 0x66, 0x2d, 0x2c, 0x2a, // 1 - u t f - - - + 0x2c, 0x65, 0x6e, 0x71, 0x3d, 0x30, 0x2e // - e n q - 0 - +}; diff --git a/SPDY/en.lproj/InfoPlist.strings b/SPDY/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/SPDY/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/SPDYUnitTests/SPDYFrameCodecTest.m b/SPDYUnitTests/SPDYFrameCodecTest.m new file mode 100644 index 0000000..8cc4edc --- /dev/null +++ b/SPDYUnitTests/SPDYFrameCodecTest.m @@ -0,0 +1,366 @@ +// +// SPDYFrameCodecTest.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import +#import "SPDYFrameEncoder.h" +#import "SPDYFrameDecoder.h" +#import "SPDYMockFrameDecoderDelegate.h" + +@interface SPDYFrameCodecTest : SenTestCase +@end + +@interface SPDYFrameDecoder (CodecTest) +- (void)didEncodeData:(NSData *)data frameEncoder:(SPDYFrameEncoder *)encoder; +@end + +@implementation SPDYFrameDecoder (CodecTest) +- (void)didEncodeData:(NSData *)data frameEncoder:(SPDYFrameEncoder *)encoder +{ + NSError *error; + [self decode:(uint8_t *)data.bytes length:data.length error:&error]; +} +@end + +@implementation SPDYFrameCodecTest +{ + SPDYMockFrameDecoderDelegate *_mock; + SPDYFrameEncoder *_encoder; + SPDYFrameDecoder *_decoder; +} + +#define AssertLastFrameClass(CLASS_NAME) \ + STAssertEqualObjects(NSStringFromClass([_mock.lastFrame class]), CLASS_NAME, @"expected mock delegate's last frame received to be of class %@, but was %@", CLASS_NAME, NSStringFromClass([_mock.lastFrame class])) + +#define AssertFramesReceivedCount(COUNT) \ + STAssertTrue(_mock.frameCount == COUNT, @"expected property framesReceived of mock delegate to contain %d elements, but had %d elements", COUNT, _mock.frameCount) + +#define AssertLastDelegateMessage(SELECTOR_NAME) \ + STAssertEqualObjects(_mock.lastDelegateMessage, SELECTOR_NAME, @"expected mock delegate's last message received to be %@ but was %@", SELECTOR_NAME, _mock.lastDelegateMessage) + +NSDictionary *testHeaders() +{ + return @{ + @":method" : @"GET", + @":path" : @"/search?q=pokemans", + @":version" : @"HTTP/1.1", + @":host" : @"www.google.com:443", + @":scheme" : @"https", + @"pokemans" : @[@"pikachu", @"charmander", @"squirtle", @"bulbasaur"] + }; +} + +- (void)setUp +{ + [super setUp]; + + _mock = [[SPDYMockFrameDecoderDelegate alloc] init]; + _decoder = [[SPDYFrameDecoder alloc] initWithDelegate:_mock]; + _encoder = [[SPDYFrameEncoder alloc] initWithDelegate:_decoder + headerCompressionLevel:9]; +} + +- (void)testDataFrame +{ + SPDYDataFrame *inFrame = [[SPDYDataFrame alloc] init]; + inFrame.streamId = arc4random() & 0x7FFFFFFF; + inFrame.last = YES; + + uint8_t data[100]; + for (uint8_t i = 0; i < 100; i++) { + data[i] = i; + } + + inFrame.data = [[NSData alloc] initWithBytes:data length:100]; + + [_encoder encodeDataFrame:inFrame]; + + AssertLastFrameClass(@"SPDYDataFrame"); + AssertFramesReceivedCount(1); + + SPDYDataFrame *outFrame = _mock.lastFrame; + + STAssertEquals(inFrame.streamId, outFrame.streamId, nil); + STAssertEquals(inFrame.last, outFrame.last, nil); + STAssertEquals(inFrame.data.length, outFrame.data.length, nil); + for (uint8_t i = 0; i < 100; i++) { + STAssertEquals(((uint8_t *)inFrame.data.bytes)[i], ((uint8_t *)outFrame.data.bytes)[i], nil); + } +} + +//- (void)testLargeDataFrame +//{ +// SPDYDataFrame *inFrame = [[SPDYDataFrame alloc] init]; +// inFrame.streamId = arc4random() & 0x7FFFFFFF; +// inFrame.last = YES; +// +// uint8_t *data = malloc(sizeof(uint8_t) * 1024 * 1024); +// for (uint8_t i = 0; i < sizeof(data); i++) { +// data[i] = i; +// } +// +// [_encoder encodeDataFrame:inFrame]; +// +// AssertLastFrameClass(@"SPDYDataFrame"); +// AssertFramesReceivedCount(1); +// +// SPDYDataFrame *outFrame = _mock.lastFrame; +// +// STAssertEquals(inFrame.streamId, outFrame.streamId, nil); +// STAssertEquals(inFrame.last, outFrame.last, nil); +// STAssertEquals(inFrame.data.length, outFrame.data.length, nil); +// for (uint8_t i = 0; i < 1024 * 1024; i++) { +// STAssertEquals(((uint8_t *)inFrame.data.bytes)[i], ((uint8_t *)outFrame.data.bytes)[i], nil); +// } +//} + +- (void)testSynStreamFrame +{ + SPDYSynStreamFrame *inFrame = [[SPDYSynStreamFrame alloc] init]; + inFrame.streamId = arc4random() & 0x7FFFFFFF; + inFrame.associatedToStreamId = arc4random() & 0x7FFFFFFF; + inFrame.unidirectional = (bool)(arc4random() & 1); + inFrame.last = (bool)(arc4random() & 1); + inFrame.priority = (uint8_t)(arc4random() & 7); + inFrame.slot = 0; + + [_encoder encodeSynStreamFrame:inFrame]; + + AssertLastFrameClass(@"SPDYSynStreamFrame"); + AssertFramesReceivedCount(1); + + SPDYSynStreamFrame *outFrame = _mock.lastFrame; + + STAssertEquals(inFrame.streamId, outFrame.streamId, nil); + STAssertEquals(inFrame.associatedToStreamId, outFrame.associatedToStreamId, nil); + STAssertEquals(inFrame.unidirectional, outFrame.unidirectional, nil); + STAssertEquals(inFrame.last, outFrame.last, nil); + STAssertEquals(inFrame.priority, outFrame.priority, nil); + STAssertEquals(inFrame.slot, outFrame.slot, nil); +} + +- (void)testSynStreamFrameWithHeaders +{ + SPDYSynStreamFrame *inFrame = [[SPDYSynStreamFrame alloc] init]; + inFrame.streamId = arc4random() & 0x7FFFFFFF; + inFrame.associatedToStreamId = arc4random() & 0x7FFFFFFF; + inFrame.unidirectional = (bool)(arc4random() & 1); + inFrame.last = (bool)(arc4random() & 1); + inFrame.priority = (uint8_t)(arc4random() & 7); + inFrame.slot = 0; + inFrame.headers = testHeaders(); + + [_encoder encodeSynStreamFrame:inFrame]; + + AssertLastFrameClass(@"SPDYSynStreamFrame"); + AssertFramesReceivedCount(1); + + SPDYSynStreamFrame *outFrame = _mock.lastFrame; + + STAssertEquals(inFrame.streamId, outFrame.streamId, nil); + STAssertEquals(inFrame.associatedToStreamId, outFrame.associatedToStreamId, nil); + STAssertEquals(inFrame.unidirectional, outFrame.unidirectional, nil); + STAssertEquals(inFrame.last, outFrame.last, nil); + STAssertEquals(inFrame.priority, outFrame.priority, nil); + STAssertEquals(inFrame.slot, outFrame.slot, nil); + for (NSString *key in inFrame.headers) { + STAssertTrue([inFrame.headers[key] isEqual:outFrame.headers[key]], nil); + } +} + +- (void)testSynReplyFrame +{ + SPDYSynReplyFrame *inFrame = [[SPDYSynReplyFrame alloc] init]; + inFrame.streamId = arc4random() & 0x7FFFFFFF; + inFrame.last = (bool)(arc4random() & 1); + + [_encoder encodeSynReplyFrame:inFrame]; + + AssertLastFrameClass(@"SPDYSynReplyFrame"); + AssertFramesReceivedCount(1); + + SPDYSynReplyFrame *outFrame = _mock.lastFrame; + + STAssertEquals(inFrame.streamId, outFrame.streamId, nil); + STAssertEquals(inFrame.last, outFrame.last, nil); +} + +- (void)testSynReplyFrameWithHeaders +{ + SPDYSynReplyFrame *inFrame = [[SPDYSynReplyFrame alloc] init]; + inFrame.streamId = arc4random() & 0x7FFFFFFF; + inFrame.last = (bool)(arc4random() & 1); + inFrame.headers = testHeaders(); + + [_encoder encodeSynReplyFrame:inFrame]; + + AssertLastFrameClass(@"SPDYSynReplyFrame"); + AssertFramesReceivedCount(1); + + SPDYSynReplyFrame *outFrame = _mock.lastFrame; + + STAssertEquals(inFrame.streamId, outFrame.streamId, nil); + STAssertEquals(inFrame.last, outFrame.last, nil); + for (NSString *key in inFrame.headers) { + STAssertTrue([inFrame.headers[key] isEqual:outFrame.headers[key]], nil); + } +} + +- (void)testRstStreamFrame +{ + SPDYRstStreamFrame *inFrame = [[SPDYRstStreamFrame alloc] init]; + inFrame.streamId = arc4random() & 0x7FFFFFFF; + inFrame.statusCode = (SPDYStreamStatus)(arc4random() % 11 + 1); + + [_encoder encodeRstStreamFrame:inFrame]; + + AssertLastFrameClass(@"SPDYRstStreamFrame"); + AssertFramesReceivedCount(1); + + SPDYRstStreamFrame *outFrame = _mock.lastFrame; + + STAssertEquals(inFrame.streamId, outFrame.streamId, nil); + STAssertEquals(inFrame.statusCode, outFrame.statusCode, nil); +} + +- (void)testSettingsFrame +{ + SPDYSettingsFrame *inFrame = [[SPDYSettingsFrame alloc] init]; + inFrame.clearSettings = (bool)(arc4random() & 1); + + int k = arc4random() % (_SPDY_SETTINGS_RANGE_END - _SPDY_SETTINGS_RANGE_START) + 1; + SPDYSettingsId chosenIds[k]; + + int j = 0; + SPDY_SETTINGS_ITERATOR(i) { + if (j < k) { + chosenIds[j] = i; + } else if ( (arc4random() / (double)UINT32_MAX) < (k / (j+1.0)) ) { + chosenIds[arc4random() % k] = i; + } + j++; + } + + bool persisted = (bool)(arc4random() & 1); + + for (int i = 0; i < k; i++) { + inFrame.settings[chosenIds[i]].set = YES; + inFrame.settings[chosenIds[i]].flags = SPDY_SETTINGS_FLAG_PERSISTED * persisted; + inFrame.settings[chosenIds[i]].value = arc4random(); + } + + [_encoder encodeSettingsFrame:inFrame]; + + AssertLastFrameClass(@"SPDYSettingsFrame"); + AssertFramesReceivedCount(1); + + SPDYSettingsFrame *outFrame = _mock.lastFrame; + + STAssertEquals(inFrame.clearSettings, outFrame.clearSettings, nil); + SPDY_SETTINGS_ITERATOR(i) { + STAssertEquals(inFrame.settings[i].set, outFrame.settings[i].set, nil); + if (inFrame.settings[i].set) { + STAssertEquals(inFrame.settings[i].flags, outFrame.settings[i].flags, nil); + STAssertEquals(inFrame.settings[i].value, outFrame.settings[i].value, nil); + } + } +} + +- (void)testPingFrame +{ + SPDYPingFrame *inFrame = [[SPDYPingFrame alloc] init]; + inFrame.id = arc4random() & 0xFFFFFFFE; + + [_encoder encodePingFrame:inFrame]; + + AssertLastFrameClass(@"SPDYPingFrame"); + AssertFramesReceivedCount(1); + + SPDYPingFrame *outFrame = _mock.lastFrame; + + STAssertEquals(inFrame.id, outFrame.id, nil); +} + +- (void)testGoAwayFrame +{ + SPDYGoAwayFrame *inFrame = [[SPDYGoAwayFrame alloc] init]; + inFrame.statusCode = (SPDYSessionStatus)(arc4random() % 3); + + [_encoder encodeGoAwayFrame:inFrame]; + + AssertLastFrameClass(@"SPDYGoAwayFrame"); + AssertFramesReceivedCount(1); + + SPDYGoAwayFrame *outFrame = _mock.lastFrame; + + STAssertEquals(inFrame.statusCode, outFrame.statusCode, nil); +} + +- (void)testHeadersFrame +{ + SPDYHeadersFrame *inFrame = [[SPDYHeadersFrame alloc] init]; + inFrame.streamId = arc4random() & 0x7FFFFFFF; + inFrame.last = (bool)(arc4random() & 1); + + [_encoder encodeHeadersFrame:inFrame]; + + AssertLastFrameClass(@"SPDYHeadersFrame"); + AssertFramesReceivedCount(1); + + SPDYHeadersFrame *outFrame = _mock.lastFrame; + + STAssertEquals(inFrame.streamId, outFrame.streamId, nil); + STAssertEquals(inFrame.last, outFrame.last, nil); +} + +- (void)testHeadersFrameWithHeaders +{ + SPDYHeadersFrame *inFrame = [[SPDYHeadersFrame alloc] init]; + inFrame.streamId = arc4random() & 0x7FFFFFFF; + inFrame.last = (bool)(arc4random() & 1); + inFrame.headers = testHeaders(); + + [_encoder encodeHeadersFrame:inFrame]; + + AssertLastFrameClass(@"SPDYHeadersFrame"); + AssertFramesReceivedCount(1); + + SPDYHeadersFrame *outFrame = _mock.lastFrame; + + STAssertEquals(inFrame.streamId, outFrame.streamId, nil); + STAssertEquals(inFrame.last, outFrame.last, nil); + for (NSString *key in inFrame.headers) { + STAssertTrue([inFrame.headers[key] isEqual:outFrame.headers[key]], nil); + } +} + +- (void)testWindowUpdateFrame +{ + SPDYWindowUpdateFrame *inFrame = [[SPDYWindowUpdateFrame alloc] init]; + inFrame.streamId = arc4random() & 0x7FFFFFFF; + inFrame.deltaWindowSize = arc4random() & 0x7FFFFFFF; + + [_encoder encodeWindowUpdateFrame:inFrame]; + + AssertLastFrameClass(@"SPDYWindowUpdateFrame"); + AssertFramesReceivedCount(1); + + SPDYWindowUpdateFrame *outFrame = _mock.lastFrame; + + STAssertEquals(inFrame.streamId, outFrame.streamId, nil); + STAssertEquals(inFrame.deltaWindowSize, outFrame.deltaWindowSize, nil); +} + +- (void)tearDown +{ + [super tearDown]; +} + +@end diff --git a/SPDYUnitTests/SPDYMockFrameDecoderDelegate.h b/SPDYUnitTests/SPDYMockFrameDecoderDelegate.h new file mode 100644 index 0000000..9b8bda8 --- /dev/null +++ b/SPDYUnitTests/SPDYMockFrameDecoderDelegate.h @@ -0,0 +1,21 @@ +// +// SPDYMockFrameDecoderDelegate.h +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import +#import "SPDYFrameDecoder.h" + +@interface SPDYMockFrameDecoderDelegate : NSObject +@property (nonatomic, strong, readonly) NSArray *framesReceived; +@property (nonatomic, strong, readonly) id lastFrame; +@property (nonatomic, readonly) NSString *lastDelegateMessage; +@property (nonatomic, readonly) NSUInteger frameCount; +@end + diff --git a/SPDYUnitTests/SPDYMockFrameDecoderDelegate.m b/SPDYUnitTests/SPDYMockFrameDecoderDelegate.m new file mode 100644 index 0000000..e4da492 --- /dev/null +++ b/SPDYUnitTests/SPDYMockFrameDecoderDelegate.m @@ -0,0 +1,50 @@ +// +// SPDYMockFrameDecoderDelegate.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import "SPDYMockFrameDecoderDelegate.h" + + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wprotocol" +@implementation SPDYMockFrameDecoderDelegate +#pragma clang diagnostic pop +{ + NSMutableArray *_framesReceived; +} + +- (id)init +{ + self = [super init]; + if (self) { + _framesReceived = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)forwardInvocation:(NSInvocation *)invocation +{ + _lastDelegateMessage = NSStringFromSelector([invocation selector]); + __unsafe_unretained SPDYFrame *frame; + [invocation getArgument:&frame atIndex:2]; + [_framesReceived addObject:frame]; +} + +- (id)lastFrame +{ + return _framesReceived.lastObject; +} + +- (NSUInteger)frameCount +{ + return _framesReceived.count; +} + +@end diff --git a/SPDYUnitTests/SPDYOriginTest.m b/SPDYUnitTests/SPDYOriginTest.m new file mode 100644 index 0000000..80cd552 --- /dev/null +++ b/SPDYUnitTests/SPDYOriginTest.m @@ -0,0 +1,120 @@ +// +// SPDYOriginTest.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import +#import "SPDYOrigin.h" + +@interface SPDYOriginTest : SenTestCase +@end + +@implementation SPDYOriginTest + +- (void)testInitEquivalency +{ + NSError *error; + NSURL *url; + NSString *originStr; + SPDYOrigin *o1, *o2, *o3, *o4, *o5, *o6, *o7; + + originStr = @"http://twitter.com"; + o1 = [[SPDYOrigin alloc] initWithString:originStr error:&error]; + STAssertTrue(error == nil, nil); + + url = [[NSURL alloc] initWithString:originStr]; + o2 = [[SPDYOrigin alloc] initWithURL:url error:&error]; + STAssertTrue(error == nil, nil); + + o3 = [[SPDYOrigin alloc] initWithScheme:@"http" + host:@"twitter.com" + port:0 + error:&error]; + STAssertTrue(error == nil, nil); + + originStr = @"http://twitter.com:80"; + o4 = [[SPDYOrigin alloc] initWithString:originStr error:&error]; + STAssertTrue(error == nil, nil); + + url = [[NSURL alloc] initWithString:originStr]; + o5 = [[SPDYOrigin alloc] initWithURL:url error:&error]; + STAssertTrue(error == nil, nil); + + o6 = [[SPDYOrigin alloc] initWithScheme:@"http" + host:@"twitter.com" + port:80 + error:&error]; + STAssertTrue(error == nil, nil); + + o7 = [o6 copy]; + + STAssertTrue([o1 isEqual:o2], nil); + STAssertTrue([o1 hash] == [o2 hash], nil); + + STAssertTrue([o2 isEqual:o3], nil); + STAssertTrue([o2 hash] == [o3 hash], nil); + + STAssertTrue([o3 isEqual:o4], nil); + STAssertTrue([o3 hash] == [o4 hash], nil); + + STAssertTrue([o4 isEqual:o5], nil); + STAssertTrue([o4 hash] == [o5 hash], nil); + + STAssertTrue([o5 isEqual:o6], nil); + STAssertTrue([o5 hash] == [o6 hash], nil); + + STAssertFalse(o6 == o7, nil); + STAssertTrue([o6 isEqual:o7], nil); + STAssertTrue([o6 hash] == [o7 hash], nil); +} + +- (void)testInitWithInvalidOrigins +{ + NSError *error = nil; + SPDYOrigin *origin = nil; + + NSArray *badOrigins = @[ + @"http://", + @"twitter.com", + @"ftp://twitter.com", + ]; + + for (NSString *originStr in badOrigins) { + error = nil; + origin = [[SPDYOrigin alloc] initWithString:originStr error:&error]; + STAssertTrue(error != nil, nil); + STAssertTrue(origin == nil, nil); + } +} + +- (void)testImmutability +{ + NSMutableString *scheme = [[NSMutableString alloc] initWithString:@"http"]; + NSMutableString *host = [[NSMutableString alloc] initWithString:@"twitter.com"]; + + SPDYOrigin *origin = [[SPDYOrigin alloc] initWithScheme:scheme + host:host + port:80 + error:nil]; + + STAssertTrue([origin.scheme isEqualToString:scheme], nil); + STAssertTrue([origin.host isEqualToString:host], nil); + NSUInteger hash1 = [origin hash]; + + [scheme appendString:@"s"]; + [host appendString:@".jp"]; + NSUInteger hash2 = [origin hash]; + + STAssertFalse([origin.scheme isEqualToString:scheme], nil); + STAssertFalse([origin.host isEqualToString:host], nil); + + STAssertTrue(hash1 == hash2, nil); +} + +@end diff --git a/SPDYUnitTests/SPDYStreamManagerTest.m b/SPDYUnitTests/SPDYStreamManagerTest.m new file mode 100644 index 0000000..3d76401 --- /dev/null +++ b/SPDYUnitTests/SPDYStreamManagerTest.m @@ -0,0 +1,153 @@ +// +// SPDYStreamManagerTest.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import +#import "SPDYStreamManager.h" +#import "SPDYStream.h" +#import "SPDYProtocol.h" + +@interface SPDYStreamManagerTest : SenTestCase +@end + +@interface SPDYStubbedProtocol : SPDYProtocol +@end + +@implementation SPDYStubbedProtocol +- (id)client +{ + return nil; +} +@end + +@interface SPDYStubbedStream : SPDYStream ++ (void)resetTestStreamIds; +- (id)initWithPriority:(uint8_t)priority; +@end + +@implementation SPDYStubbedStream +{ + SPDYProtocol *_retainedProtocol; +} +static SPDYStreamId _nextStreamId; + ++ (void)resetTestStreamIds +{ + _nextStreamId = 1; +} + +- (id)initWithPriority:(uint8_t)priority +{ + self = [super init]; + if (self) { + self.priority = priority; + self.streamId = _nextStreamId; + _nextStreamId += 1; + if (self.local) { + _retainedProtocol = [[SPDYStubbedProtocol alloc] init]; + self.protocol = _retainedProtocol; + } + } + return self; +} + +- (bool)local +{ + return self.streamId % 2 == 1; +} + +@end + +@implementation SPDYStreamManagerTest +{ + SPDYStreamManager *_manager; + NSUInteger _numStreams; +} + +- (void)setUp +{ + [super setUp]; + _manager = [[SPDYStreamManager alloc] init]; + _numStreams = 100; + + [SPDYStubbedStream resetTestStreamIds]; + for (NSUInteger i = 0; i < _numStreams; i++) { + SPDYStream *stream = [[SPDYStubbedStream alloc] initWithPriority:((uint8_t)arc4random() % 8)]; + _manager[stream.streamId] = stream; + } +} + +- (void)tearDown +{ + [super tearDown]; +} + +- (void)testFastIterationByPriority +{ + SPDYStream *prevStream; + NSUInteger streamCount = 0; + NSUInteger localStreamCount = 0; + NSUInteger remoteStreamCount = 0; + for (SPDYStream *stream in _manager) { + streamCount++; + stream.local ? localStreamCount++ : remoteStreamCount++; + if (prevStream) { + STAssertTrue(prevStream.priority <= stream.priority, nil); + } + prevStream = stream; + } + STAssertEquals(streamCount, _numStreams, nil); + STAssertEquals(streamCount, _manager.count, nil); + STAssertEquals(localStreamCount, _manager.localCount, nil); + STAssertEquals(remoteStreamCount, _manager.remoteCount, nil); +} + +- (void)testSubscriptAccessors +{ + for (SPDYStream *stream in _manager) { + STAssertEquals(_manager[stream.streamId], stream, nil); + if (stream.protocol) { + STAssertEquals(_manager[stream.protocol], stream, nil); + } + } +} + +- (void)testStreamRemoval +{ + SPDYStream *one = _manager[4]; + SPDYStream *two = _manager[5]; + + STAssertNotNil(one, nil); + STAssertNotNil(two, nil); + + NSUInteger prevRemoteStreamCount = _manager.remoteCount; + [_manager removeStreamWithStreamId:4]; + STAssertEquals(prevRemoteStreamCount - 1, _manager.remoteCount, nil); + + NSUInteger prevLocalStreamCount = _manager.localCount; + [_manager removeStreamForProtocol:two.protocol]; + STAssertEquals(prevLocalStreamCount - 1, _manager.localCount, nil); + + STAssertNil(_manager[4], nil); + STAssertNil(_manager[5], nil); + + SPDYStream *prevStream; + NSUInteger streamCount = 0; + for (SPDYStream *stream in _manager) { + streamCount++; + if (prevStream) { + STAssertTrue(prevStream.priority <= stream.priority, nil); + } + prevStream = stream; + } + STAssertEquals(streamCount, _numStreams - 2, nil); +} + +@end diff --git a/SPDYUnitTests/SPDYStreamTest.m b/SPDYUnitTests/SPDYStreamTest.m new file mode 100644 index 0000000..f1efe33 --- /dev/null +++ b/SPDYUnitTests/SPDYStreamTest.m @@ -0,0 +1,168 @@ +// +// SPDYStreamTest.m +// SPDY +// +// Copyright (c) 2013 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Michael Schore and Jeffrey Pinner. +// + +#import +#import "SPDYStream.h" + +@interface SPDYStreamTest : SenTestCase +@end + +typedef void (^SPDYAsyncTestCallback)(); + +@interface SPDYMockStreamDataDelegate : NSObject +@property (nonatomic, readonly) NSData *data; +@property (nonatomic, copy) SPDYAsyncTestCallback callback; +@end + +@implementation SPDYMockStreamDataDelegate +{ + NSMutableData *_data; +} + +- (id)init +{ + self = [super init]; + if (self) { + _data = [NSMutableData new]; + } + return self; +} + +- (void)streamDataAvailable:(SPDYStream *)stream +{ + [_data appendData:[stream readData:10 error:nil]]; +} + +- (void)streamFinished:(SPDYStream *)stream +{ + _callback(); +} + +@end + +@implementation SPDYStreamTest + +static NSMutableData *_uploadData; + ++ (void)setUp +{ + _uploadData = [[NSMutableData alloc] initWithCapacity:26 * 16]; + NSData *alpha = [@"abcdefghijklmnopqrstuvwyz" dataUsingEncoding:NSUTF8StringEncoding]; + [_uploadData appendData:alpha]; + for (int i = 0; i < 4; i++) { + [_uploadData appendData:_uploadData]; + } +} + +- (void)testBodyWithData +{ + NSMutableData *producedData = [[NSMutableData alloc] initWithCapacity:26 * 16]; + SPDYStream *spdyStream = [SPDYStream new]; + spdyStream.data = _uploadData; + + __block bool finished = NO; + + STAssertTrue([NSThread isMainThread], @"dispatch must occur from main thread"); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + STAssertFalse([NSThread isMainThread], @"stream must be scheduled off main thread"); + + // Run off-thread runloop + while(spdyStream.hasDataAvailable) { + [producedData appendData:[spdyStream readData:10 error:nil]]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + finished = YES; + }); + }); + + // Run main thread runloop + while(!finished) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + + STAssertTrue([producedData isEqualToData:_uploadData], nil); +} + +- (void)testBodyStreamViaData +{ + SPDYMockStreamDataDelegate *mockDataDelegate = [SPDYMockStreamDataDelegate new]; + SPDYStream *spdyStream = [SPDYStream new]; + spdyStream.dataDelegate = mockDataDelegate; + spdyStream.dataStream = [[NSInputStream alloc] initWithData:_uploadData]; + + __block bool finished = NO; + mockDataDelegate.callback = ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + finished = YES; + }); + }; + + STAssertTrue([NSThread isMainThread], @"dispatch must occur from main thread"); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + STAssertFalse([NSThread isMainThread], @"stream must be scheduled off main thread"); + + [spdyStream performSelector:@selector(_scheduleCFReadStream)]; + + // Run off-thread runloop + while(!finished) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + }); + + // Run main thread runloop + while(!finished) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + + STAssertTrue([mockDataDelegate.data isEqualToData:_uploadData], nil); +} + +- (void)testBodyStreamViaFile +{ + SPDYMockStreamDataDelegate *mockDataDelegate = [SPDYMockStreamDataDelegate new]; + SPDYStream *spdyStream = [SPDYStream new]; + NSString *filePath = @"/var/tmp/com.twitter.spdy.StreamTestData"; + NSError *error = nil; + NSDataWritingOptions fileOptions = NSDataWritingAtomic | NSDataWritingFileProtectionNone; + STAssertTrue([_uploadData writeToFile:filePath options:fileOptions error:&error], + error.localizedDescription); + spdyStream.dataDelegate = mockDataDelegate; + spdyStream.dataStream = [[NSInputStream alloc] initWithFileAtPath:filePath]; + + __block bool finished = NO; + mockDataDelegate.callback = ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + finished = YES; + }); + }; + + STAssertTrue([NSThread isMainThread], @"dispatch must occur from main thread"); + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + STAssertFalse([NSThread isMainThread], @"stream must be scheduled off main thread"); + + [spdyStream performSelector:@selector(_scheduleCFReadStream)]; + + // Run off-thread runloop + while(!finished) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + }); + + // Run main thread runloop + while(!finished) { + [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; + } + + STAssertTrue([mockDataDelegate.data isEqualToData:_uploadData], nil); +} + +@end diff --git a/SPDYUnitTests/SPDYUnitTests-Info.plist b/SPDYUnitTests/SPDYUnitTests-Info.plist new file mode 100644 index 0000000..323cac7 --- /dev/null +++ b/SPDYUnitTests/SPDYUnitTests-Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + com.twitter.${PRODUCT_NAME:rfc1034identifier} + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/SPDYUnitTests/SPDYUnitTests-Prefix.pch b/SPDYUnitTests/SPDYUnitTests-Prefix.pch new file mode 100644 index 0000000..fecd37e --- /dev/null +++ b/SPDYUnitTests/SPDYUnitTests-Prefix.pch @@ -0,0 +1,7 @@ +// +// Prefix header for all source files of the 'SPDYUnitTests' target in the 'SPDYUnitTests' project +// + +#ifdef __OBJC__ + #import +#endif diff --git a/SPDYUnitTests/en.lproj/InfoPlist.strings b/SPDYUnitTests/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..477b28f --- /dev/null +++ b/SPDYUnitTests/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ +