From 3735a3358ecb21e3025ce5ad3b0aeacf3a062e7e Mon Sep 17 00:00:00 2001 From: Kevin Goodier Date: Mon, 18 May 2015 16:42:35 -0700 Subject: [PATCH 1/9] Basic support for push streams. Split the merging of stream headers from the processing of the response. Modify SPDYStream to support initializing as a push stream, and processing the "request" side of that (essentially the SYN_STREAM). Duplicate the original request but replace the URL. Basic push tests are included here, but more are coming later. The ability for the app to actually hook up to a pushed stream is also coming later. --- SPDY.xcodeproj/project.pbxproj | 84 ++++++------ SPDY/SPDYSession.m | 60 ++++++-- SPDY/SPDYStream.h | 11 +- SPDY/SPDYStream.m | 148 +++++++++++++++++--- SPDYUnitTests/SPDYMockSessionTestBase.m | 2 +- SPDYUnitTests/SPDYServerPushTest.m | 174 ++++++++++++++++++++++++ SPDYUnitTests/SPDYSessionTest.m | 9 +- SPDYUnitTests/SPDYStreamTest.m | 38 ++++-- SPDYUnitTests/SPDYURLRequestTest.m | 2 +- 9 files changed, 438 insertions(+), 90 deletions(-) create mode 100644 SPDYUnitTests/SPDYServerPushTest.m diff --git a/SPDY.xcodeproj/project.pbxproj b/SPDY.xcodeproj/project.pbxproj index 8e99104..053a56d 100644 --- a/SPDY.xcodeproj/project.pbxproj +++ b/SPDY.xcodeproj/project.pbxproj @@ -98,29 +98,22 @@ 5C2229591952257800CAF160 /* SPDYURLRequestTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C2229581952257800CAF160 /* SPDYURLRequestTest.m */; }; 5C2A211D19F9CA0E00D0EA76 /* SPDYLoggingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C2A211C19F9CA0E00D0EA76 /* SPDYLoggingTest.m */; }; 5C427F111A1D57890072403D /* SPDYStopwatchTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C427F101A1D57890072403D /* SPDYStopwatchTest.m */; }; - 5C48CF7F1B0A65910082F7EF /* SPDYCacheStoragePolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C48CF7D1B0A65910082F7EF /* SPDYCacheStoragePolicy.h */; }; - 5C48CF801B0A65910082F7EF /* SPDYCacheStoragePolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C48CF7D1B0A65910082F7EF /* SPDYCacheStoragePolicy.h */; }; - 5C48CF811B0A65910082F7EF /* SPDYCacheStoragePolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C48CF7E1B0A65910082F7EF /* SPDYCacheStoragePolicy.m */; }; - 5C48CF821B0A65910082F7EF /* SPDYCacheStoragePolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C48CF7E1B0A65910082F7EF /* SPDYCacheStoragePolicy.m */; }; - 5C48CF831B0A65910082F7EF /* SPDYCacheStoragePolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C48CF7E1B0A65910082F7EF /* SPDYCacheStoragePolicy.m */; }; - 5C48CF841B0A65DD0082F7EF /* SPDYCacheStoragePolicy.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C48CF7D1B0A65910082F7EF /* SPDYCacheStoragePolicy.h */; }; - 5C48CF931B0A6A180082F7EF /* SPDYMockSessionTestBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C48CF921B0A6A180082F7EF /* SPDYMockSessionTestBase.m */; }; + 5C48CF8E1B0A684C0082F7EF /* SPDYCacheStoragePolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C48CF8C1B0A68400082F7EF /* SPDYCacheStoragePolicy.m */; }; + 5C48CF8F1B0A684C0082F7EF /* SPDYCacheStoragePolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C48CF8C1B0A68400082F7EF /* SPDYCacheStoragePolicy.m */; }; + 5C48CF901B0A684D0082F7EF /* SPDYCacheStoragePolicy.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C48CF8C1B0A68400082F7EF /* SPDYCacheStoragePolicy.m */; }; 5C5EA46E1A119B630058FB64 /* SPDYOriginEndpoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5EA46A1A119B630058FB64 /* SPDYOriginEndpoint.m */; }; 5C5EA46F1A119B630058FB64 /* SPDYOriginEndpoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5EA46A1A119B630058FB64 /* SPDYOriginEndpoint.m */; }; 5C5EA4701A119B630058FB64 /* SPDYOriginEndpoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5EA46A1A119B630058FB64 /* SPDYOriginEndpoint.m */; }; 5C5EA4731A119C950058FB64 /* SPDYMockOriginEndpointManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5EA4721A119C950058FB64 /* SPDYMockOriginEndpointManager.m */; }; 5C5EA4751A119CAB0058FB64 /* SPDYSocketTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5EA4741A119CAB0058FB64 /* SPDYSocketTest.m */; }; - 5C6B0D291A3A3E8400334BFA /* SPDYCanonicalRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C6B0D271A3A3E8400334BFA /* SPDYCanonicalRequest.h */; }; - 5C6B0D2A1A3A3E8400334BFA /* SPDYCanonicalRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C6B0D271A3A3E8400334BFA /* SPDYCanonicalRequest.h */; }; - 5C6B0D2B1A3A3E8400334BFA /* SPDYCanonicalRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C6B0D271A3A3E8400334BFA /* SPDYCanonicalRequest.h */; }; - 5C6B0D2C1A3A3E8400334BFA /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6B0D281A3A3E8400334BFA /* SPDYCanonicalRequest.m */; }; - 5C6B0D2D1A3A3E8400334BFA /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6B0D281A3A3E8400334BFA /* SPDYCanonicalRequest.m */; }; - 5C6B0D2E1A3A3E8400334BFA /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6B0D281A3A3E8400334BFA /* SPDYCanonicalRequest.m */; }; 5C6D809A1BC44C19003AF2E0 /* SPDYURLCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D80991BC44C19003AF2E0 /* SPDYURLCacheTest.m */; settings = {ASSET_TAGS = (); }; }; + 5C6D809E1BC45569003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D809C1BC4554D003AF2E0 /* SPDYCanonicalRequest.m */; settings = {ASSET_TAGS = (); }; }; + 5C6D809F1BC45569003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D809C1BC4554D003AF2E0 /* SPDYCanonicalRequest.m */; settings = {ASSET_TAGS = (); }; }; + 5C6D80A01BC4556A003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D809C1BC4554D003AF2E0 /* SPDYCanonicalRequest.m */; settings = {ASSET_TAGS = (); }; }; + 5C7E662F1B0533360037AD91 /* SPDYProtocolTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C7E662E1B0533360037AD91 /* SPDYProtocolTest.m */; }; 5C9A0BD01A363BDC00CF2D3D /* SPDYOriginEndpointManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A0BCF1A363BDC00CF2D3D /* SPDYOriginEndpointManager.m */; }; 5C9A0BD11A363BDC00CF2D3D /* SPDYOriginEndpointManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A0BCF1A363BDC00CF2D3D /* SPDYOriginEndpointManager.m */; }; 5C9A0BD21A363BDC00CF2D3D /* SPDYOriginEndpointManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A0BCF1A363BDC00CF2D3D /* SPDYOriginEndpointManager.m */; }; - 5CA0B9C61A6454950068ABD9 /* SPDYProtocolTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CA0B9C51A6454950068ABD9 /* SPDYProtocolTest.m */; }; 5CA0B9C81A6486F10068ABD9 /* SPDYSettingsStoreTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CA0B9C71A6486F10068ABD9 /* SPDYSettingsStoreTest.m */; }; 5CE43CE11AD74FC900E73FAC /* SPDYMetadata+Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CE43CDF1AD74E2200E73FAC /* SPDYMetadata+Utils.m */; }; 5CE43CE21AD74FC900E73FAC /* SPDYMetadata+Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CE43CDF1AD74E2200E73FAC /* SPDYMetadata+Utils.m */; }; @@ -129,12 +122,14 @@ 5CF0A2CC1A0952D900B6D141 /* SPDYMockURLProtocolClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CF0A2CB1A0952D900B6D141 /* SPDYMockURLProtocolClient.m */; }; 7774C1318AB029C6BCEF84D6 /* SPDYSessionTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 7774C2026A4DE9957D75F629 /* SPDYSessionTest.m */; }; 7774C1F1E544793907908882 /* SPDYMockFrameEncoderDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7774C69089A6978113F0C275 /* SPDYMockFrameEncoderDelegate.m */; }; + 7774C2FBEA6973E90CAE7005 /* SPDYMockSessionTestBase.m in Sources */ = {isa = PBXBuildFile; fileRef = 7774C193AC525BC3A79F2853 /* SPDYMockSessionTestBase.m */; }; 7774C868441241542B0A90C0 /* SPDYStopwatch.m in Sources */ = {isa = PBXBuildFile; fileRef = 7774CE34A3AA067D98DA7ECC /* SPDYStopwatch.m */; }; 7774CA1FA1F4A59CA0906BB7 /* SPDYSocket+SPDYSocketMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 7774C0ECD0C6E5D73FB38752 /* SPDYSocket+SPDYSocketMock.m */; }; 7774CC29BBD86413798C1425 /* SPDYStopwatch.h in Headers */ = {isa = PBXBuildFile; fileRef = 7774CE030BB898D2BE3D320D /* SPDYStopwatch.h */; }; 7774CD12A73EA9ABAE521441 /* SPDYStopwatch.m in Sources */ = {isa = PBXBuildFile; fileRef = 7774CE34A3AA067D98DA7ECC /* SPDYStopwatch.m */; }; 7774CD9416661E40D76713F5 /* SPDYStopwatch.h in Headers */ = {isa = PBXBuildFile; fileRef = 7774CE030BB898D2BE3D320D /* SPDYStopwatch.h */; }; 7774CDD84A5D07F8DE5B8684 /* SPDYStopwatch.m in Sources */ = {isa = PBXBuildFile; fileRef = 7774CE34A3AA067D98DA7ECC /* SPDYStopwatch.m */; }; + 7774CEA5AB5D1536D0CDCEA4 /* SPDYServerPushTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 7774C803F0948788139CD2C1 /* SPDYServerPushTest.m */; }; 7774CF887055793F373F0D5E /* SPDYStopwatch.h in Headers */ = {isa = PBXBuildFile; fileRef = 7774CE030BB898D2BE3D320D /* SPDYStopwatch.h */; }; 8B40E8AA1AE1B95C000A0F0F /* SPDYProtocol+Project.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B40E8A81AE1B95C000A0F0F /* SPDYProtocol+Project.h */; }; 8B40E8AB1AE1B95C000A0F0F /* SPDYProtocol+Project.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B40E8A81AE1B95C000A0F0F /* SPDYProtocol+Project.h */; }; @@ -195,21 +190,19 @@ 5C2229581952257800CAF160 /* SPDYURLRequestTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYURLRequestTest.m; sourceTree = ""; }; 5C2A211C19F9CA0E00D0EA76 /* SPDYLoggingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYLoggingTest.m; sourceTree = ""; }; 5C427F101A1D57890072403D /* SPDYStopwatchTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYStopwatchTest.m; sourceTree = ""; }; - 5C48CF7D1B0A65910082F7EF /* SPDYCacheStoragePolicy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYCacheStoragePolicy.h; sourceTree = ""; }; - 5C48CF7E1B0A65910082F7EF /* SPDYCacheStoragePolicy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYCacheStoragePolicy.m; sourceTree = ""; }; - 5C48CF911B0A6A180082F7EF /* SPDYMockSessionTestBase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYMockSessionTestBase.h; sourceTree = ""; }; - 5C48CF921B0A6A180082F7EF /* SPDYMockSessionTestBase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYMockSessionTestBase.m; sourceTree = ""; }; + 5C48CF8B1B0A68400082F7EF /* SPDYCacheStoragePolicy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYCacheStoragePolicy.h; sourceTree = ""; }; + 5C48CF8C1B0A68400082F7EF /* SPDYCacheStoragePolicy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYCacheStoragePolicy.m; sourceTree = ""; }; 5C5EA4691A119B630058FB64 /* SPDYOriginEndpoint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYOriginEndpoint.h; sourceTree = ""; }; 5C5EA46A1A119B630058FB64 /* SPDYOriginEndpoint.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYOriginEndpoint.m; sourceTree = ""; }; 5C5EA4711A119C950058FB64 /* SPDYMockOriginEndpointManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYMockOriginEndpointManager.h; sourceTree = ""; }; 5C5EA4721A119C950058FB64 /* SPDYMockOriginEndpointManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYMockOriginEndpointManager.m; sourceTree = ""; }; 5C5EA4741A119CAB0058FB64 /* SPDYSocketTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYSocketTest.m; sourceTree = ""; }; - 5C6B0D271A3A3E8400334BFA /* SPDYCanonicalRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYCanonicalRequest.h; sourceTree = ""; }; - 5C6B0D281A3A3E8400334BFA /* SPDYCanonicalRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYCanonicalRequest.m; sourceTree = ""; }; 5C6D80991BC44C19003AF2E0 /* SPDYURLCacheTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYURLCacheTest.m; sourceTree = ""; }; + 5C6D809B1BC4554D003AF2E0 /* SPDYCanonicalRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYCanonicalRequest.h; sourceTree = ""; }; + 5C6D809C1BC4554D003AF2E0 /* SPDYCanonicalRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYCanonicalRequest.m; sourceTree = ""; }; + 5C7E662E1B0533360037AD91 /* SPDYProtocolTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYProtocolTest.m; sourceTree = ""; }; 5C9A0BCB1A363B7700CF2D3D /* SPDYOriginEndpointManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYOriginEndpointManager.h; sourceTree = ""; }; 5C9A0BCF1A363BDC00CF2D3D /* SPDYOriginEndpointManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYOriginEndpointManager.m; sourceTree = ""; }; - 5CA0B9C51A6454950068ABD9 /* SPDYProtocolTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYProtocolTest.m; sourceTree = ""; }; 5CA0B9C71A6486F10068ABD9 /* SPDYSettingsStoreTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYSettingsStoreTest.m; sourceTree = ""; }; 5CE43CDE1AD74E2200E73FAC /* SPDYMetadata+Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SPDYMetadata+Utils.h"; sourceTree = ""; }; 5CE43CDF1AD74E2200E73FAC /* SPDYMetadata+Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SPDYMetadata+Utils.m"; sourceTree = ""; }; @@ -217,12 +210,15 @@ 5CF0A2CA1A0952BA00B6D141 /* SPDYMockURLProtocolClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYMockURLProtocolClient.h; sourceTree = ""; }; 5CF0A2CB1A0952D900B6D141 /* SPDYMockURLProtocolClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYMockURLProtocolClient.m; sourceTree = ""; }; 7774C0ECD0C6E5D73FB38752 /* SPDYSocket+SPDYSocketMock.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SPDYSocket+SPDYSocketMock.m"; sourceTree = ""; }; + 7774C193AC525BC3A79F2853 /* SPDYMockSessionTestBase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYMockSessionTestBase.m; sourceTree = ""; }; 7774C2026A4DE9957D75F629 /* SPDYSessionTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYSessionTest.m; sourceTree = ""; }; 7774C69089A6978113F0C275 /* SPDYMockFrameEncoderDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYMockFrameEncoderDelegate.m; sourceTree = ""; }; 7774C7E1AF717FC36B7F15B6 /* SPDYSocket+SPDYSocketMock.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SPDYSocket+SPDYSocketMock.h"; sourceTree = ""; }; + 7774C803F0948788139CD2C1 /* SPDYServerPushTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYServerPushTest.m; sourceTree = ""; }; 7774CD0A3295C8E314D6E3FF /* SPDYMockFrameEncoderDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYMockFrameEncoderDelegate.h; sourceTree = ""; }; 7774CE030BB898D2BE3D320D /* SPDYStopwatch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYStopwatch.h; sourceTree = ""; }; 7774CE34A3AA067D98DA7ECC /* SPDYStopwatch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYStopwatch.m; sourceTree = ""; }; + 7774CFEA3D0DAF374D7C7654 /* SPDYMockSessionTestBase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYMockSessionTestBase.h; sourceTree = ""; }; 8B40E8A81AE1B95C000A0F0F /* SPDYProtocol+Project.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SPDYProtocol+Project.h"; 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 = ""; }; @@ -296,7 +292,8 @@ 5CF0A2C81A089BC500B6D141 /* SPDYMetadataTest.m */, 5C04570419B043CB009E0AC2 /* SPDYOriginEndpointTest.m */, 0679F3CE186217FC006F122E /* SPDYOriginTest.m */, - 5CA0B9C51A6454950068ABD9 /* SPDYProtocolTest.m */, + 5C7E662E1B0533360037AD91 /* SPDYProtocolTest.m */, + 7774C803F0948788139CD2C1 /* SPDYServerPushTest.m */, 25959A3E1937DE3900FC9731 /* SPDYSessionManagerTest.m */, 7774C2026A4DE9957D75F629 /* SPDYSessionTest.m */, 5CA0B9C71A6486F10068ABD9 /* SPDYSettingsStoreTest.m */, @@ -323,10 +320,10 @@ 7774C69089A6978113F0C275 /* SPDYMockFrameEncoderDelegate.m */, 5C5EA4711A119C950058FB64 /* SPDYMockOriginEndpointManager.h */, 5C5EA4721A119C950058FB64 /* SPDYMockOriginEndpointManager.m */, - 5C48CF911B0A6A180082F7EF /* SPDYMockSessionTestBase.h */, - 5C48CF921B0A6A180082F7EF /* SPDYMockSessionTestBase.m */, 7774C7E1AF717FC36B7F15B6 /* SPDYSocket+SPDYSocketMock.h */, 7774C0ECD0C6E5D73FB38752 /* SPDYSocket+SPDYSocketMock.m */, + 7774CFEA3D0DAF374D7C7654 /* SPDYMockSessionTestBase.h */, + 7774C193AC525BC3A79F2853 /* SPDYMockSessionTestBase.m */, 5CF0A2CA1A0952BA00B6D141 /* SPDYMockURLProtocolClient.h */, 5CF0A2CB1A0952D900B6D141 /* SPDYMockURLProtocolClient.m */, ); @@ -390,10 +387,10 @@ 069D0E99168268F10037D8AF /* NSURLRequest+SPDYURLRequest.h */, 069D0E9A168268F10037D8AF /* NSURLRequest+SPDYURLRequest.m */, D2CC14B816179B43002E37CF /* SPDY-Prefix.pch */, - 5C48CF7D1B0A65910082F7EF /* SPDYCacheStoragePolicy.h */, - 5C48CF7E1B0A65910082F7EF /* SPDYCacheStoragePolicy.m */, - 5C6B0D271A3A3E8400334BFA /* SPDYCanonicalRequest.h */, - 5C6B0D281A3A3E8400334BFA /* SPDYCanonicalRequest.m */, + 5C48CF8B1B0A68400082F7EF /* SPDYCacheStoragePolicy.h */, + 5C48CF8C1B0A68400082F7EF /* SPDYCacheStoragePolicy.m */, + 5C6D809B1BC4554D003AF2E0 /* SPDYCanonicalRequest.h */, + 5C6D809C1BC4554D003AF2E0 /* SPDYCanonicalRequest.m */, 062EA63E175D4CD3003BC1CE /* SPDYCommonLogger.h */, 062EA63F175D4CD3003BC1CE /* SPDYCommonLogger.m */, 062EA63C175D1A15003BC1CE /* SPDYDefinitions.h */, @@ -457,10 +454,8 @@ 0540DAAD19CB800C00673796 /* SPDYLogger.h in Headers */, 0540DAAE19CB801900673796 /* SPDYProtocol.h in Headers */, 0540DAAF19CB802600673796 /* SPDYTLSTrustEvaluator.h in Headers */, - 5C6B0D2A1A3A3E8400334BFA /* SPDYCanonicalRequest.h in Headers */, 0540DAA919CB7FEB00673796 /* SPDYCommonLogger.h in Headers */, 7774CD9416661E40D76713F5 /* SPDYStopwatch.h in Headers */, - 5C48CF801B0A65910082F7EF /* SPDYCacheStoragePolicy.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -474,10 +469,8 @@ 05C7CEBA19CB45760032D681 /* SPDYLogger.h in Headers */, 05C7CEBB19CB45820032D681 /* SPDYProtocol.h in Headers */, 05C7CEBC19CB458F0032D681 /* SPDYTLSTrustEvaluator.h in Headers */, - 5C6B0D291A3A3E8400334BFA /* SPDYCanonicalRequest.h in Headers */, 0540DAAA19CB7FEB00673796 /* SPDYCommonLogger.h in Headers */, 7774CC29BBD86413798C1425 /* SPDYStopwatch.h in Headers */, - 5C48CF7F1B0A65910082F7EF /* SPDYCacheStoragePolicy.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -491,10 +484,8 @@ 06811C9B1715DC85000D1677 /* SPDYLogger.h in Headers */, 064A05C716F7C313008C7D08 /* SPDYProtocol.h in Headers */, 06E7BF131824371F004DB65D /* SPDYTLSTrustEvaluator.h in Headers */, - 5C6B0D2B1A3A3E8400334BFA /* SPDYCanonicalRequest.h in Headers */, 062EA640175D4CD3003BC1CE /* SPDYCommonLogger.h in Headers */, 7774CF887055793F373F0D5E /* SPDYStopwatch.h in Headers */, - 5C48CF841B0A65DD0082F7EF /* SPDYCacheStoragePolicy.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -593,9 +584,9 @@ projectDirPath = ""; projectRoot = ""; targets = ( - 064EFB1216715C9F002F0AEC /* SPDYUnitTests */, 0651EBE716F3F7C700CE44D2 /* SPDY.iphoneos */, 0651EC0716F3F7E500CE44D2 /* SPDY.macosx */, + 064EFB1216715C9F002F0AEC /* SPDYUnitTests */, 0652631216F7B6360081868F /* SPDY */, ); }; @@ -672,14 +663,12 @@ files = ( 5C6D809A1BC44C19003AF2E0 /* SPDYURLCacheTest.m in Sources */, 5C0456FF19B033E9009E0AC2 /* SPDYSocketOps.m in Sources */, - 5C48CF931B0A6A180082F7EF /* SPDYMockSessionTestBase.m in Sources */, 06FDA20616717DF100137DBD /* SPDYSocket.m in Sources */, 5CA0B9C81A6486F10068ABD9 /* SPDYSettingsStoreTest.m in Sources */, 5C210A0A1A5F48C500ADB538 /* SPDYSessionPool.m in Sources */, 5C2A211D19F9CA0E00D0EA76 /* SPDYLoggingTest.m in Sources */, 06FDA20916717DF100137DBD /* SPDYFrame.m in Sources */, 06FDA20B16717DF100137DBD /* SPDYFrameDecoder.m in Sources */, - 5CA0B9C61A6454950068ABD9 /* SPDYProtocolTest.m in Sources */, 0679F3CF186217FC006F122E /* SPDYOriginTest.m in Sources */, 06FDA20D16717DF100137DBD /* SPDYProtocol.m in Sources */, 06FDA20F16717DF100137DBD /* SPDYSession.m in Sources */, @@ -688,7 +677,6 @@ 060C235E17CE9FCE000B4E9C /* SPDYStreamManagerTest.m in Sources */, 5C427F111A1D57890072403D /* SPDYStopwatchTest.m in Sources */, 06290995169E4D9700E35A82 /* SPDYHeaderBlockCompressor.m in Sources */, - 5C48CF811B0A65910082F7EF /* SPDYCacheStoragePolicy.m in Sources */, 06FDA21216717DF100137DBD /* SPDYHeaderBlockDecompressor.m in Sources */, 5C04570519B043CB009E0AC2 /* SPDYOriginEndpointTest.m in Sources */, 5C5EA4751A119CAB0058FB64 /* SPDYSocketTest.m in Sources */, @@ -711,9 +699,13 @@ 061C8E9517C5954400D22083 /* SPDYStreamManager.m in Sources */, 06B290CE1861018900540A03 /* SPDYOrigin.m in Sources */, 7774C1F1E544793907908882 /* SPDYMockFrameEncoderDelegate.m in Sources */, - 5C6B0D2C1A3A3E8400334BFA /* SPDYCanonicalRequest.m in Sources */, 7774CA1FA1F4A59CA0906BB7 /* SPDYSocket+SPDYSocketMock.m in Sources */, + 7774CEA5AB5D1536D0CDCEA4 /* SPDYServerPushTest.m in Sources */, + 5C6D80A01BC4556A003AF2E0 /* SPDYCanonicalRequest.m in Sources */, 7774C1318AB029C6BCEF84D6 /* SPDYSessionTest.m in Sources */, + 5C48CF901B0A684D0082F7EF /* SPDYCacheStoragePolicy.m in Sources */, + 5C7E662F1B0533360037AD91 /* SPDYProtocolTest.m in Sources */, + 7774C2FBEA6973E90CAE7005 /* SPDYMockSessionTestBase.m in Sources */, 7774CD12A73EA9ABAE521441 /* SPDYStopwatch.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -727,12 +719,10 @@ 5C5EA46F1A119B630058FB64 /* SPDYOriginEndpoint.m in Sources */, 0651EC3D16F3FA1400CE44D2 /* SPDYFrame.m in Sources */, 0651EC3E16F3FA1400CE44D2 /* SPDYFrameDecoder.m in Sources */, - 5C6B0D2D1A3A3E8400334BFA /* SPDYCanonicalRequest.m in Sources */, 0651EC3F16F3FA1400CE44D2 /* SPDYFrameEncoder.m in Sources */, 0651EC4016F3FA1400CE44D2 /* SPDYHeaderBlockCompressor.m in Sources */, 0651EC4116F3FA1400CE44D2 /* SPDYHeaderBlockDecompressor.m in Sources */, 0651EC4216F3FA1400CE44D2 /* SPDYProtocol.m in Sources */, - 5C48CF821B0A65910082F7EF /* SPDYCacheStoragePolicy.m in Sources */, 0651EC4316F3FA1400CE44D2 /* SPDYSession.m in Sources */, 0651EC4416F3FA1400CE44D2 /* SPDYSessionManager.m in Sources */, 0651EC4516F3FA1400CE44D2 /* SPDYSettingsStore.m in Sources */, @@ -741,7 +731,9 @@ 062EA643175D4CD3003BC1CE /* SPDYCommonLogger.m in Sources */, 5CE43CE21AD74FC900E73FAC /* SPDYMetadata+Utils.m in Sources */, 061C8E9617C5954400D22083 /* SPDYStreamManager.m in Sources */, + 5C6D809E1BC45569003AF2E0 /* SPDYCanonicalRequest.m in Sources */, 5C210A0B1A5F48C500ADB538 /* SPDYSessionPool.m in Sources */, + 5C48CF8E1B0A684C0082F7EF /* SPDYCacheStoragePolicy.m in Sources */, 06B290CF1861018A00540A03 /* SPDYOrigin.m in Sources */, 5C04570019B033E9009E0AC2 /* SPDYSocketOps.m in Sources */, 7774C868441241542B0A90C0 /* SPDYStopwatch.m in Sources */, @@ -757,12 +749,10 @@ 5C5EA4701A119B630058FB64 /* SPDYOriginEndpoint.m in Sources */, 0651EC2316F3FA0B00CE44D2 /* SPDYFrame.m in Sources */, 0651EC2416F3FA0B00CE44D2 /* SPDYFrameDecoder.m in Sources */, - 5C6B0D2E1A3A3E8400334BFA /* SPDYCanonicalRequest.m in Sources */, 0651EC2516F3FA0B00CE44D2 /* SPDYFrameEncoder.m in Sources */, 0651EC2616F3FA0B00CE44D2 /* SPDYHeaderBlockCompressor.m in Sources */, 0651EC2716F3FA0B00CE44D2 /* SPDYHeaderBlockDecompressor.m in Sources */, 0651EC2816F3FA0B00CE44D2 /* SPDYProtocol.m in Sources */, - 5C48CF831B0A65910082F7EF /* SPDYCacheStoragePolicy.m in Sources */, 0651EC2916F3FA0B00CE44D2 /* SPDYSession.m in Sources */, 0651EC2A16F3FA0B00CE44D2 /* SPDYSessionManager.m in Sources */, 0651EC2B16F3FA0B00CE44D2 /* SPDYSettingsStore.m in Sources */, @@ -771,7 +761,9 @@ 062EA645175D4CD3003BC1CE /* SPDYCommonLogger.m in Sources */, 5CE43CE31AD74FCA00E73FAC /* SPDYMetadata+Utils.m in Sources */, 061C8E9817C5954400D22083 /* SPDYStreamManager.m in Sources */, + 5C6D809F1BC45569003AF2E0 /* SPDYCanonicalRequest.m in Sources */, 5C210A0C1A5F48C500ADB538 /* SPDYSessionPool.m in Sources */, + 5C48CF8F1B0A684C0082F7EF /* SPDYCacheStoragePolicy.m in Sources */, 06B290D21861018A00540A03 /* SPDYOrigin.m in Sources */, 5C04570319B033EA009E0AC2 /* SPDYSocketOps.m in Sources */, 7774CDD84A5D07F8DE5B8684 /* SPDYStopwatch.m in Sources */, @@ -839,7 +831,7 @@ DSTROOT = /tmp/SPDY_iphoneos.dst; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "SPDY.iphoneos/SPDY.iphoneos-Prefix.pch"; - IPHONEOS_DEPLOYMENT_TARGET = 5.1.1; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; ONLY_ACTIVE_ARCH = NO; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -859,7 +851,7 @@ DSTROOT = /tmp/SPDY_iphoneos.dst; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "SPDY.iphoneos/SPDY.iphoneos-Prefix.pch"; - IPHONEOS_DEPLOYMENT_TARGET = 5.1.1; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; ONLY_ACTIVE_ARCH = NO; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -1028,7 +1020,7 @@ GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "SPDY.iphoneos/SPDY.iphoneos-Prefix.pch"; - IPHONEOS_DEPLOYMENT_TARGET = 5.1.1; + IPHONEOS_DEPLOYMENT_TARGET = 6.0; ONLY_ACTIVE_ARCH = NO; OTHER_LDFLAGS = "-ObjC"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/SPDY/SPDYSession.m b/SPDY/SPDYSession.m index dc8b647..09bd982 100644 --- a/SPDY/SPDYSession.m +++ b/SPDY/SPDYSession.m @@ -35,7 +35,7 @@ // buffer. #define DEFAULT_WINDOW_SIZE 65536 #define INITIAL_INPUT_BUFFER_SIZE 65536 -#define LOCAL_MAX_CONCURRENT_STREAMS 0 +#define LOCAL_MAX_CONCURRENT_STREAMS 32 #define REMOTE_MAX_CONCURRENT_STREAMS INT32_MAX @interface SPDYSession () @@ -568,7 +568,9 @@ - (void)didReadSynStreamFrame:(SPDYSynStreamFrame *)synStreamFrame frameDecoder: */ SPDYStreamId streamId = synStreamFrame.streamId; - SPDY_DEBUG(@"received SYN_STREAM.%u", streamId); + SPDYStreamId associatedToStreamId = synStreamFrame.associatedToStreamId; + SPDYStream *associatedStream = _activeStreams[associatedToStreamId]; + SPDY_DEBUG(@"received SYN_STREAM.%u associated %u", streamId, associatedToStreamId); // Stream-IDs must be monotonically increasing if (streamId <= _lastGoodStreamId) { @@ -576,26 +578,50 @@ - (void)didReadSynStreamFrame:(SPDYSynStreamFrame *)synStreamFrame frameDecoder: return; } + // Session must be active and still have room for incoming streams 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; + // If a client receives a server push stream with stream-id 0, + // it MUST issue a session error (Section 2.4.1) with the status code PROTOCOL_ERROR. + // Also the SYN_STREAM MUST include an Associated-To-Stream-ID, + // and MUST set the FLAG_UNIDIRECTIONAL flag. + // TODO: confirm shutting down the connection is the right thing to do for all these cases. + // If no associated stream, send GOAWAY or just refuse stream? + if (streamId == 0 || associatedToStreamId == 0 || !synStreamFrame.unidirectional || !associatedStream) { + [self _closeWithStatus:SPDY_SESSION_PROTOCOL_ERROR]; + return; + } + + // TODO: Browsers receiving a pushed response MUST validate that the server is authorized to + // push the URL using the browser same-origin policy. For example, a SPDY connection to + // www.foo.com is generally not permitted to push a response for www.evil.com. + + SPDYStream *stream = [[SPDYStream alloc] initWithAssociatedStream:associatedStream + priority:synStreamFrame.priority]; + stream.metadata.rxBytes += synStreamFrame.encodedLength; SPDYTimeInterval now = [SPDYStopwatch currentSystemTime]; stream.metadata.timeStreamRequestStarted = now; stream.metadata.timeStreamRequestLastHeader = now; stream.metadata.timeStreamResponseStarted = now; - stream.metadata.timeStreamResponseLastHeader = now; + + [stream startWithStreamId:streamId + sendWindowSize:_initialSendWindowSize + receiveWindowSize:_initialReceiveWindowSize]; _lastGoodStreamId = streamId; _activeStreams[streamId] = stream; + + [stream mergeHeaders:synStreamFrame.headers]; + [stream didReceivePushRequest]; + + if (!stream.closed) { + stream.remoteSideClosed = synStreamFrame.last; + } } - (void)didReadSynReplyFrame:(SPDYSynReplyFrame *)synReplyFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder @@ -634,7 +660,8 @@ - (void)didReadSynReplyFrame:(SPDYSynReplyFrame *)synReplyFrame frameDecoder:(SP return; } - [stream didReceiveResponse:synReplyFrame.headers]; + [stream mergeHeaders:synReplyFrame.headers]; + [stream didReceiveResponse]; if (!stream.closed) { stream.remoteSideClosed = synReplyFrame.last; @@ -792,7 +819,6 @@ - (void)didReadHeadersFrame:(SPDYHeadersFrame *)headersFrame frameDecoder:(SPDYF if (stream) { stream.metadata.rxBytes += headersFrame.encodedLength; - stream.metadata.timeStreamResponseLastHeader = [SPDYStopwatch currentSystemTime]; } if (!stream || stream.remoteSideClosed) { @@ -800,7 +826,19 @@ - (void)didReadHeadersFrame:(SPDYHeadersFrame *)headersFrame frameDecoder:(SPDYF return; } - stream.remoteSideClosed = headersFrame.last; + // If the server sends a HEADER frame containing duplicate headers + // with a previous HEADERS frame for the same stream, the client must + // issue a stream error (Section 2.4.2) with error code PROTOCOL ERROR. + [stream mergeHeaders:headersFrame.headers]; + + if (!stream.closed) { + // This is the "response" for a push request + if (!stream.local) { + stream.metadata.timeStreamResponseLastHeader = [SPDYStopwatch currentSystemTime]; + [stream didReceiveResponse]; + } + stream.remoteSideClosed = headersFrame.last; + } } - (void)didReadWindowUpdateFrame:(SPDYWindowUpdateFrame *)windowUpdateFrame frameDecoder:(SPDYFrameDecoder *)frameDecoder diff --git a/SPDY/SPDYStream.h b/SPDY/SPDYStream.h index ef586e1..7c2c7f7 100644 --- a/SPDY/SPDYStream.h +++ b/SPDY/SPDYStream.h @@ -30,8 +30,10 @@ @property (nonatomic) SPDYMetadata *metadata; @property (nonatomic) NSData *data; @property (nonatomic) NSInputStream *dataStream; +@property (nonatomic) NSDictionary *headers; @property (nonatomic, weak) NSURLRequest *request; @property (nonatomic, weak) SPDYProtocol *protocol; +@property (nonatomic, weak) SPDYStream *associatedStream; @property (nonatomic) SPDYStreamId streamId; @property (nonatomic) uint8_t priority; @property (nonatomic) bool local; @@ -47,13 +49,18 @@ @property (nonatomic) uint32_t receiveWindowSizeLowerBound; - (instancetype)initWithProtocol:(SPDYProtocol *)protocol; -- (void)startWithStreamId:(SPDYStreamId)id sendWindowSize:(uint32_t)sendWindowSize receiveWindowSize:(uint32_t)receiveWindowSize; +- (instancetype)initWithAssociatedStream:(SPDYStream *)associatedStream + priority:(uint8_t)priority; +- (void)startWithStreamId:(SPDYStreamId)id sendWindowSize:(uint32_t)sendWindowSize + receiveWindowSize:(uint32_t)receiveWindowSize; - (bool)reset; - (NSData *)readData:(NSUInteger)length error:(NSError **)pError; - (void)cancel; - (void)closeWithError:(NSError *)error; - (void)abortWithError:(NSError *)error status:(SPDYStreamStatus)status; -- (void)didReceiveResponse:(NSDictionary *)headers; +- (void)mergeHeaders:(NSDictionary *)newHeaders; +- (void)didReceiveResponse; +- (void)didReceivePushRequest; - (void)didLoadData:(NSData *)data; - (void)markBlocked; - (void)markUnblocked; diff --git a/SPDY/SPDYStream.m b/SPDY/SPDYStream.m index f18f252..ce38a65 100644 --- a/SPDY/SPDYStream.m +++ b/SPDY/SPDYStream.m @@ -51,6 +51,7 @@ @implementation SPDYStream NSData *_data; NSString *_dataFile; NSInputStream *_dataStream; + NSDictionary *_headers; NSRunLoop *_runLoop; CFReadStreamRef _dataStreamRef; CFRunLoopRef _runLoopRef; @@ -61,9 +62,13 @@ @implementation SPDYStream bool _compressedResponse; bool _writeStreamOpened; int _zlibStreamStatus; + bool _ignoreHeaders; SPDYStopwatch *_blockedStopwatch; SPDYTimeInterval _blockedElapsed; bool _blocked; + + NSURLRequest *_pushRequest; // stored because we need a strong reference, _request is weak. + NSHTTPURLResponse *_response; } - (instancetype)initWithProtocol:(SPDYProtocol *)protocol @@ -80,8 +85,34 @@ - (instancetype)initWithProtocol:(SPDYProtocol *)protocol _remoteSideClosed = NO; _compressedResponse = NO; _receivedReply = NO; + _delegate = nil; + _metadata = [[SPDYMetadata alloc] init]; + _blockedStopwatch = [[SPDYStopwatch alloc] init]; + _associatedStream = nil; + + _metadata.timeStreamCreated = [SPDYStopwatch currentSystemTime]; + } + return self; +} + +- (id)initWithAssociatedStream:(SPDYStream *)associatedStream priority:(uint8_t)priority +{ + self = [super init]; + if (self) { + _protocol = nil; + _client = nil; + _request = nil; + _priority = priority; + _dispatchAttempts = 0; + _local = NO; + _localSideClosed = YES; // this is a push request, our side has nothing to say + _remoteSideClosed = NO; + _compressedResponse = NO; + _receivedReply = NO; + _delegate = associatedStream.delegate; _metadata = [[SPDYMetadata alloc] init]; _blockedStopwatch = [[SPDYStopwatch alloc] init]; + _associatedStream = associatedStream; _metadata.timeStreamCreated = [SPDYStopwatch currentSystemTime]; } @@ -374,10 +405,42 @@ - (NSData *)readData:(NSUInteger)length error:(NSError **)pError return nil; } -- (void)didReceiveResponse:(NSDictionary *)headers +- (void)mergeHeaders:(NSDictionary *)newHeaders +{ + // If the server sends a HEADERS frame after sending a data frame + // for the same stream, the client MAY ignore the HEADERS frame. + // Ignoring the HEADERS frame after a data frame prevents handling of HTTP's + // trailing headers (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.40). + if (_ignoreHeaders) { + SPDY_WARNING(@"ignoring trailing headers: %@", newHeaders); + return; + } + + // See if any headers collide with previous + if ([[NSSet setWithArray:[_headers allKeys]] intersectsSet:[NSSet setWithArray:[newHeaders allKeys]]]) { + NSError *error = SPDY_STREAM_ERROR(SPDYStreamProtocolError, @"received duplicate headers"); + [self abortWithError:error status:SPDY_STREAM_PROTOCOL_ERROR]; + return; + } + + // Merge raw headers + NSMutableDictionary *merged = [NSMutableDictionary dictionaryWithDictionary:_headers]; + [merged addEntriesFromDictionary:newHeaders]; + _headers = merged; +} + +- (void)didReceiveResponse { + if (_receivedReply) { + SPDY_WARNING(@"already received a response for stream %u", _streamId); + return; + } + + NSDictionary *headers = _headers; _receivedReply = YES; + _ignoreHeaders = NO; + // Pull out and validate statusCode for later use NSInteger statusCode = [headers[@":status"] intValue]; if (statusCode < 100 || statusCode > 599) { NSDictionary *info = @{ NSLocalizedDescriptionKey: @"invalid http response code" }; @@ -388,6 +451,7 @@ - (void)didReceiveResponse:(NSDictionary *)headers return; } + // Pull out and validate version for later use NSString *version = headers[@":version"]; if (!version) { NSDictionary *info = @{ NSLocalizedDescriptionKey: @"response missing version header" }; @@ -398,6 +462,7 @@ - (void)didReceiveResponse:(NSDictionary *)headers return; } + // Create a "clean" set of headers for the NSURLResponse NSMutableDictionary *allHTTPHeaders = [[NSMutableDictionary alloc] init]; for (NSString *key in headers) { if (![key hasPrefix:@":"]) { @@ -410,7 +475,7 @@ - (void)didReceiveResponse:(NSDictionary *)headers } } - NSURL *requestURL = _protocol.request.URL; + NSURL *requestURL = _request.URL; BOOL cookiesOn = NO; NSHTTPCookieStorage *cookieStore = nil; @@ -418,7 +483,7 @@ - (void)didReceiveResponse:(NSDictionary *)headers if (config) { switch (config.HTTPCookieAcceptPolicy) { case NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain: - if ([_protocol.request.URL.host compare:_protocol.request.mainDocumentURL.host options:NSCaseInsensitiveSearch] != NSOrderedSame) { + if ([_request.URL.host compare:_request.mainDocumentURL.host options:NSCaseInsensitiveSearch] != NSOrderedSame) { break; } // else, fall through case NSHTTPCookieAcceptPolicyAlways: @@ -429,7 +494,7 @@ - (void)didReceiveResponse:(NSDictionary *)headers break; } } else { - cookiesOn = _protocol.request.HTTPShouldHandleCookies; + cookiesOn = _request.HTTPShouldHandleCookies; cookieStore = [NSHTTPCookieStorage sharedHTTPCookieStorage]; } @@ -448,7 +513,7 @@ - (void)didReceiveResponse:(NSDictionary *)headers [cookieStore setCookies:cookies forURL:requestURL - mainDocumentURL:_protocol.request.mainDocumentURL]; + mainDocumentURL:_request.mainDocumentURL]; } } @@ -461,13 +526,12 @@ - (void)didReceiveResponse:(NSDictionary *)headers [SPDYMetadata setMetadata:_metadata forAssociatedDictionary:allHTTPHeaders]; - NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:requestURL - statusCode:statusCode - HTTPVersion:version - headerFields:allHTTPHeaders]; + _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) { @@ -482,7 +546,7 @@ - (void)didReceiveResponse:(NSDictionary *)headers // shouldStartLoadWithRequest callback, the hostname gets stripped out. By flattening // the NSURL with absoluteString, we can avoid that. This is observed in iOS8 but not iOS7. NSURL *finalRedirectURL = [NSURL URLWithString:redirectURL.absoluteString]; - NSMutableURLRequest *redirect = [_protocol.request mutableCopy]; + NSMutableURLRequest *redirect = [_request mutableCopy]; redirect.URL = finalRedirectURL; redirect.SPDYPriority = _request.SPDYPriority; redirect.SPDYBodyFile = _request.SPDYBodyFile; @@ -507,14 +571,55 @@ - (void)didReceiveResponse:(NSDictionary *)headers redirect.SPDYBodyStream = nil; } - [_client URLProtocol:_protocol wasRedirectedToRequest:redirect redirectResponse:response]; + if (_client) { + [_client URLProtocol:_protocol wasRedirectedToRequest:redirect redirectResponse:_response]; + } + + return; + } + + if (_client) { + NSURLCacheStoragePolicy cachePolicy = SPDYCacheStoragePolicy(_request, _response); + [_client URLProtocol:_protocol + didReceiveResponse:_response + cacheStoragePolicy:cachePolicy]; + } +} + +- (void)didReceivePushRequest +{ + NSAssert(!_local, @"should only be called for pushed streams"); + + // Validate :scheme, :host, and :path for pushed responses, and create the cloned request + // The SYN_STREAM MUST include headers for ":scheme", ":host", + // ":path", which represent the URL for the resource being pushed. + NSString *scheme = _headers[@":scheme"]; + NSString *host = _headers[@":host"]; + NSString *path = _headers[@":path"]; + if (!scheme || !host || !path) { + SPDY_WARNING(@"SYN_STREAM missing :scheme, :host, and :path headers for stream %u", _streamId); + NSError *error = SPDY_STREAM_ERROR(SPDYStreamProtocolError, @"missing :scheme, :host, or :path header"); + [self abortWithError:error status:SPDY_STREAM_PROTOCOL_ERROR]; return; } - NSURLCacheStoragePolicy cachePolicy = SPDYCacheStoragePolicy(_request, response); - [_client URLProtocol:_protocol - didReceiveResponse:response - cacheStoragePolicy:cachePolicy]; + // Because pushed responses have no request, they have no request headers associated with + // them. At the framing layer, SPDY pushed streams contain an "associated-stream-id" which + // indicates the requested stream for which the pushed stream is related. The pushed + // stream inherits all of the headers from the associated-stream-id with the exception + // of ":host", ":scheme", and ":path", which are provided as part of the pushed response + // stream headers. The browser MUST store these inherited and implied request headers + // with the cached resource. + NSURL *pushURL = [[NSURL alloc] initWithScheme:scheme host:host path:path]; + NSMutableURLRequest *requestCopy = [NSMutableURLRequest requestWithURL:pushURL + cachePolicy:NSURLRequestUseProtocolCachePolicy + timeoutInterval:_request.timeoutInterval]; + requestCopy.allHTTPHeaderFields = _request.allHTTPHeaderFields; + requestCopy.HTTPMethod = @"GET"; + requestCopy.SPDYPriority = (NSUInteger)_priority; + + _pushRequest = [SPDYProtocol canonicalRequestForRequest:requestCopy]; + _request = _pushRequest; // need a strong reference for _request's weak one } - (void)didLoadData:(NSData *)data @@ -522,6 +627,9 @@ - (void)didLoadData:(NSData *)data NSUInteger dataLength = data.length; if (dataLength == 0) return; + // No more header merging after this point + _ignoreHeaders = YES; + if (_compressedResponse) { _zlibStream.avail_in = (uInt)dataLength; _zlibStream.next_in = (uint8_t *)data.bytes; @@ -545,7 +653,9 @@ - (void)didLoadData:(NSData *)data NSUInteger inflatedLength = DECOMPRESSED_CHUNK_LENGTH - _zlibStream.avail_out; inflatedData.length = inflatedLength; if (inflatedLength > 0) { - [_client URLProtocol:_protocol didLoadData:inflatedData]; + if (_client) { + [_client URLProtocol:_protocol didLoadData:inflatedData]; + } } // This can happen if the decompressed data is size N * DECOMPRESSED_CHUNK_LENGTH, @@ -567,7 +677,9 @@ - (void)didLoadData:(NSData *)data } } else { NSData *dataCopy = [[NSData alloc] initWithBytes:data.bytes length:dataLength]; - [_client URLProtocol:_protocol didLoadData:dataCopy]; + if (_client) { + [_client URLProtocol:_protocol didLoadData:dataCopy]; + } } } diff --git a/SPDYUnitTests/SPDYMockSessionTestBase.m b/SPDYUnitTests/SPDYMockSessionTestBase.m index 9514cce..c56ff1e 100644 --- a/SPDYUnitTests/SPDYMockSessionTestBase.m +++ b/SPDYUnitTests/SPDYMockSessionTestBase.m @@ -34,7 +34,7 @@ - (id)init return self; } -- (void)streamCanceled:(SPDYStream *)stream status:(SPDYStreamStatus)status; +- (void)streamCanceled:(SPDYStream *)stream status:(SPDYStreamStatus)status { _calledStreamCanceled++; _lastStream = stream; diff --git a/SPDYUnitTests/SPDYServerPushTest.m b/SPDYUnitTests/SPDYServerPushTest.m new file mode 100644 index 0000000..672b1ef --- /dev/null +++ b/SPDYUnitTests/SPDYServerPushTest.m @@ -0,0 +1,174 @@ +// +// SPDYServerPushTest.m +// SPDY +// +// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Klemen Verdnik on 6/10/14. +// Modified by Kevin Goodier on 9/19/14. +// + +#import +#import "SPDYOrigin.h" +#import "SPDYSession.h" +#import "SPDYSocket+SPDYSocketMock.h" +#import "SPDYFrame.h" +#import "SPDYProtocol.h" +#import "NSURLRequest+SPDYURLRequest.h" +#import "SPDYMockFrameEncoderDelegate.h" +#import "SPDYMockFrameDecoderDelegate.h" +#import "SPDYMockSessionTestBase.h" + +@interface SPDYServerPushTest : SPDYMockSessionTestBase +@end + +@implementation SPDYServerPushTest +{ +} + +#pragma mark Test Helpers + +- (void)setUp +{ + [super setUp]; +} + +- (void)tearDown +{ + [super tearDown]; +} + +#pragma mark Early push error cases tests + +- (void)testSYNStreamWithStreamIDZeroRespondsWithSessionError +{ + // Exchange initial SYN_STREAM and SYN_REPLY + [self mockSynStreamAndReplyWithId:1 last:NO]; + + // Send SYN_STREAM from server to client + [self mockServerSynStreamWithId:0 last:NO]; + + // If a client receives a server push stream with stream-id 0, it MUST issue a session error + // (Section 2.4.2) with the status code PROTOCOL_ERROR. + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)2); + XCTAssertTrue([_mockDecoderDelegate.framesReceived[0] isKindOfClass:[SPDYGoAwayFrame class]]); + XCTAssertTrue([_mockDecoderDelegate.framesReceived[1] isKindOfClass:[SPDYRstStreamFrame class]]); + XCTAssertEqual(((SPDYGoAwayFrame *)_mockDecoderDelegate.framesReceived[0]).statusCode, SPDY_SESSION_PROTOCOL_ERROR); + XCTAssertEqual(((SPDYGoAwayFrame *)_mockDecoderDelegate.framesReceived[0]).lastGoodStreamId, (SPDYStreamId)0); + XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.framesReceived[1]).statusCode, SPDY_STREAM_PROTOCOL_ERROR); + XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.framesReceived[1]).streamId, (SPDYStreamId)1); +} + +- (void)testSYNStreamWithUnidirectionalFlagUnsetRespondsWithSessionError +{ + // Exchange initial SYN_STREAM and SYN_REPLY + [self mockSynStreamAndReplyWithId:1 last:NO]; + + // Simulate a server Tx stream SYN_STREAM request (opening a push stream) that's associated + // with the stream that the client created. + SPDYSynStreamFrame *synStreamFrame = [[SPDYSynStreamFrame alloc] init]; + synStreamFrame.streamId = 2; + synStreamFrame.unidirectional = NO; + synStreamFrame.last = NO; + synStreamFrame.headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/pushed", + @":status":@"200", @":version":@"http/1.1", @"PushHeader":@"PushValue"}; + synStreamFrame.associatedToStreamId = 1; + + [_testEncoderDelegate clear]; + [_testEncoder encodeSynStreamFrame:synStreamFrame error:nil]; + [self makeSessionReadData:_testEncoderDelegate.lastEncodedData]; + + // @@@ Confirm this is right behavior + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)2); + XCTAssertTrue([_mockDecoderDelegate.framesReceived[0] isKindOfClass:[SPDYGoAwayFrame class]]); + XCTAssertTrue([_mockDecoderDelegate.framesReceived[1] isKindOfClass:[SPDYRstStreamFrame class]]); + XCTAssertEqual(((SPDYGoAwayFrame *)_mockDecoderDelegate.framesReceived[0]).statusCode, SPDY_SESSION_PROTOCOL_ERROR); + XCTAssertEqual(((SPDYGoAwayFrame *)_mockDecoderDelegate.framesReceived[0]).lastGoodStreamId, (SPDYStreamId)0); + XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.framesReceived[1]).statusCode, SPDY_STREAM_PROTOCOL_ERROR); + XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.framesReceived[1]).streamId, (SPDYStreamId)1); +} + +- (void)testSYNStreamWithAssociatedStreamIdZeroRespondsWithSessionError +{ + // Exchange initial SYN_STREAM and SYN_REPLY + [self mockSynStreamAndReplyWithId:1 last:NO]; + + // Send SYN_STREAM from server to client + SPDYSynStreamFrame *synStreamFrame = [[SPDYSynStreamFrame alloc] init]; + synStreamFrame.streamId = 2; + synStreamFrame.unidirectional = YES; + synStreamFrame.last = NO; + synStreamFrame.headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/pushed", + @":status":@"200", @":version":@"http/1.1", @"PushHeader":@"PushValue"}; + synStreamFrame.associatedToStreamId = 0; + [_testEncoderDelegate clear]; + XCTAssertTrue([_testEncoder encodeSynStreamFrame:synStreamFrame error:nil] > 0); + [self makeSessionReadData:_testEncoderDelegate.lastEncodedData]; + + // @@@ Confirm this is right behavior + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)2); + XCTAssertTrue([_mockDecoderDelegate.framesReceived[0] isKindOfClass:[SPDYGoAwayFrame class]]); + XCTAssertTrue([_mockDecoderDelegate.framesReceived[1] isKindOfClass:[SPDYRstStreamFrame class]]); + XCTAssertEqual(((SPDYGoAwayFrame *)_mockDecoderDelegate.framesReceived[0]).statusCode, SPDY_SESSION_PROTOCOL_ERROR); + XCTAssertEqual(((SPDYGoAwayFrame *)_mockDecoderDelegate.framesReceived[0]).lastGoodStreamId, (SPDYStreamId)0); + XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.framesReceived[1]).statusCode, SPDY_STREAM_PROTOCOL_ERROR); + XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.framesReceived[1]).streamId, (SPDYStreamId)1); +} + +- (void)testSYNStreamWithNoSchemeHeaderRespondsWithReset + { + // Exchange initial SYN_STREAM and SYN_REPLY + [self mockSynStreamAndReplyWithId:1 last:NO]; + + NSDictionary *headers = @{/*@":scheme":@"http", */@":host":@"mocked", @":path":@"/pushed"}; + [self mockServerSynStreamWithId:2 last:NO headers:headers]; + + // When a client receives a SYN_STREAM from the server without a the ':host', ':scheme', and + // ':path' headers in the Name/Value section, it MUST reply with a RST_STREAM with error + // code HTTP_PROTOCOL_ERROR. + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)1); + XCTAssertTrue([_mockDecoderDelegate.lastFrame isKindOfClass:[SPDYRstStreamFrame class]]); + XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.lastFrame).statusCode, SPDY_STREAM_PROTOCOL_ERROR); + XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.lastFrame).streamId, (SPDYStreamId)2); + } + +- (void)testSYNStreamAndAHeadersFrameWithDuplicatesRespondsWithReset +{ + // Exchange initial SYN_STREAM and SYN_REPLY + [self mockSynStreamAndReplyWithId:1 last:NO]; + + // Send SYN_STREAM from server to client + [self mockServerSynStreamWithId:2 last:NO]; + + NSDictionary *headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"hello", @"PushHeader2":@"PushValue2"}; + [self mockServerHeadersFrameWithId:2 headers:headers last:NO]; + + // If the server sends a HEADER frame containing duplicate headers with a previous HEADERS + // frame for the same stream, the client must issue a stream error (Section 2.4.2) with error + // code PROTOCOL ERROR. + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)1); + XCTAssertTrue([_mockDecoderDelegate.lastFrame isKindOfClass:[SPDYRstStreamFrame class]]); + XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.lastFrame).statusCode, SPDY_STREAM_PROTOCOL_ERROR); + XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.lastFrame).streamId, (SPDYStreamId)2); +} + +#pragma mark Simple push callback tests + +- (void)testSYNStreamAfterAssociatedStreamClosesRespondsWithGoAway +{ + // Exchange initial SYN_STREAM and SYN_REPLY + [self mockSynStreamAndReplyWithId:1 last:NO]; + + // Close original + [self mockServerDataFrameWithId:1 length:1 last:YES]; + + // Send SYN_STREAM from server to client + [self mockServerSynStreamWithId:2 last:NO]; + + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)1); + XCTAssertTrue([_mockDecoderDelegate.lastFrame isKindOfClass:[SPDYGoAwayFrame class]]); +} + +@end diff --git a/SPDYUnitTests/SPDYSessionTest.m b/SPDYUnitTests/SPDYSessionTest.m index a57deba..3faae79 100644 --- a/SPDYUnitTests/SPDYSessionTest.m +++ b/SPDYUnitTests/SPDYSessionTest.m @@ -374,7 +374,8 @@ - (void)testMergeHeadersWithLocationAnd200DoesRedirect @"location":@"/newpath"}; NSURL *redirectUrl = [NSURL URLWithString:@"https://mocked/newpath"]; - [stream didReceiveResponse:headers]; + [stream mergeHeaders:headers]; + [stream didReceiveResponse]; XCTAssertTrue(_mockURLProtocolClient.calledWasRedirectedToRequest); NSURLRequest *redirectRequest = _mockURLProtocolClient.lastRedirectedRequest; @@ -405,7 +406,8 @@ - (void)testMergeHeadersWithLocationAnd302DoesRedirectToGET @"location":@"https://mocked2/newpath"}; NSURL *redirectUrl = [NSURL URLWithString:@"https://mocked2/newpath"]; - [stream didReceiveResponse:headers]; + [stream mergeHeaders:headers]; + [stream didReceiveResponse]; XCTAssertTrue(_mockURLProtocolClient.calledWasRedirectedToRequest); NSURLRequest *redirectRequest = _mockURLProtocolClient.lastRedirectedRequest; @@ -432,7 +434,8 @@ - (void)testMergeHeadersWithLocationAnd303DoesRedirectToGET @"location":@"/newpath?param=value&foo=1"}; NSURL *redirectUrl = [NSURL URLWithString:@"https://mocked/newpath?param=value&foo=1"]; - [stream didReceiveResponse:headers]; + [stream mergeHeaders:headers]; + [stream didReceiveResponse]; XCTAssertTrue(_mockURLProtocolClient.calledWasRedirectedToRequest); NSURLRequest *redirectRequest = _mockURLProtocolClient.lastRedirectedRequest; diff --git a/SPDYUnitTests/SPDYStreamTest.m b/SPDYUnitTests/SPDYStreamTest.m index 1ef9cef..ebb5efe 100644 --- a/SPDYUnitTests/SPDYStreamTest.m +++ b/SPDYUnitTests/SPDYStreamTest.m @@ -1,4 +1,3 @@ -// // SPDYStreamTest.m // SPDY // @@ -89,15 +88,32 @@ - (void)testStreamingWithStream XCTAssertEqual(_mockURLProtocolClient.lastError.code, (errorCode)); \ } while (0) +- (void)testMergeHeadersCollisionDoesAbort +{ + SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol]]; + [stream startWithStreamId:1 sendWindowSize:1024 receiveWindowSize:1024]; + + NSDictionary *headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/init", + @":status":@"200", @":version":@"http/1.1"}; + NSDictionary *headersDup = @{@":scheme":@"http"}; + + [stream mergeHeaders:headers]; + XCTAssertFalse(_mockURLProtocolClient.calledDidFailWithError); + + [stream mergeHeaders:headersDup]; + SPDYAssertStreamError(SPDYStreamErrorDomain, SPDYStreamProtocolError); +} + - (void)testReceiveResponseMissingStatusCodeDoesAbort { SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol]]; [stream startWithStreamId:1 sendWindowSize:1024 receiveWindowSize:1024]; NSDictionary *headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/init", - @":version":@"http/1.1"}; + @":version":@"http/1.1"}; - [stream didReceiveResponse:headers]; + [stream mergeHeaders:headers]; + [stream didReceiveResponse]; SPDYAssertStreamError(NSURLErrorDomain, NSURLErrorBadServerResponse); } @@ -109,7 +125,8 @@ - (void)testReceiveResponseInvalidStatusCodeDoesAbort NSDictionary *headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/init", @":status":@"99", @":version":@"http/1.1"}; - [stream didReceiveResponse:headers]; + [stream mergeHeaders:headers]; + [stream didReceiveResponse]; SPDYAssertStreamError(NSURLErrorDomain, NSURLErrorBadServerResponse); } @@ -120,7 +137,9 @@ - (void)testReceiveResponseMissingVersionDoesAbort NSDictionary *headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/init", @":status":@"200"}; - [stream didReceiveResponse:headers]; + + [stream mergeHeaders:headers]; + [stream didReceiveResponse]; SPDYAssertStreamError(NSURLErrorDomain, NSURLErrorBadServerResponse); } @@ -133,7 +152,8 @@ - (void)testReceiveResponseDoesSucceed @":status":@"200", @":version":@"http/1.1", @"Header1":@"Value1", @"HeaderMany":@[@"ValueMany1", @"ValueMany2"]}; - [stream didReceiveResponse:headers]; + [stream mergeHeaders:headers]; + [stream didReceiveResponse]; XCTAssertTrue(_mockURLProtocolClient.calledDidReceiveResponse); NSHTTPURLResponse *response = _mockURLProtocolClient.lastResponse; @@ -161,7 +181,8 @@ - (void)testReceiveResponseWithLocationDoesRedirect @"location":@"newpath"}; NSURL *redirectUrl = [NSURL URLWithString:@"http://mocked/newpath"]; - [stream didReceiveResponse:headers]; + [stream mergeHeaders:headers]; + [stream didReceiveResponse]; XCTAssertTrue(_mockURLProtocolClient.calledWasRedirectedToRequest); XCTAssertEqualObjects(_mockURLProtocolClient.lastRedirectedRequest.URL.absoluteString, redirectUrl.absoluteString); @@ -189,7 +210,8 @@ - (void)testReceiveResponseWithLocationAnd303DoesRedirect @"location":@"https://mocked2/newpath"}; NSURL *redirectUrl = [NSURL URLWithString:@"https://mocked2/newpath"]; - [stream didReceiveResponse:headers]; + [stream mergeHeaders:headers]; + [stream didReceiveResponse]; XCTAssertTrue(_mockURLProtocolClient.calledWasRedirectedToRequest); XCTAssertEqualObjects(_mockURLProtocolClient.lastRedirectedRequest.URL.absoluteString, redirectUrl.absoluteString); diff --git a/SPDYUnitTests/SPDYURLRequestTest.m b/SPDYUnitTests/SPDYURLRequestTest.m index dd952e4..822d853 100644 --- a/SPDYUnitTests/SPDYURLRequestTest.m +++ b/SPDYUnitTests/SPDYURLRequestTest.m @@ -549,7 +549,7 @@ - (void)testEqualityForHTTPShouldHandleCookiesDifferentIsNo EQUALITYTEST_SETUP(); request1.HTTPShouldHandleCookies = YES; request2.HTTPShouldHandleCookies = NO; - + XCTAssertFalse([request1 isEqual:request2]); } From 0a221e4743ec405d7b43ba7c1a6cfcf0d4764272 Mon Sep 17 00:00:00 2001 From: Kevin Goodier Date: Tue, 10 Feb 2015 11:40:51 -0800 Subject: [PATCH 2/9] Add push stream manager. The push stream manager is a per-origin cache of currently open push streams. It serves as the URLProtocolClient in order to buffer lifetime events like the response, data chunks, and finalization. New requests coming from the app are checked against this cache, matching the canonical URL. If a match is found, the stream is removed from the push cache and appropriate URLProtocolClient catch-up callbacks are made. The manager also maintains a mapping of associated streams to push streams. If the original (associated) stream is cancelled, all unclaimed, in-progress push streams are cancelled. The only way to cancel a single in-progress push stream is to start a new NSURLRequest to hook it up to the push stream, then cancel it. --- SPDY.xcodeproj/project.pbxproj | 36 +- SPDY/SPDYProtocol.m | 29 +- SPDY/SPDYPushStreamManager.h | 25 ++ SPDY/SPDYPushStreamManager.m | 296 +++++++++++++++ SPDY/SPDYSessionManager.h | 4 + SPDY/SPDYSessionManager.m | 2 + SPDY/SPDYStream.h | 10 +- SPDY/SPDYStream.m | 8 + SPDYUnitTests/SPDYMockSessionTestBase.h | 5 + SPDYUnitTests/SPDYMockSessionTestBase.m | 16 +- SPDYUnitTests/SPDYPushStreamManagerTest.m | 207 ++++++++++ SPDYUnitTests/SPDYServerPushTest.m | 439 +++++++++++++++++++++- SPDYUnitTests/SPDYSessionManagerTest.m | 18 +- SPDYUnitTests/SPDYSessionTest.m | 16 +- SPDYUnitTests/SPDYStreamTest.m | 14 +- 15 files changed, 1076 insertions(+), 49 deletions(-) create mode 100644 SPDY/SPDYPushStreamManager.h create mode 100644 SPDY/SPDYPushStreamManager.m create mode 100644 SPDYUnitTests/SPDYPushStreamManagerTest.m diff --git a/SPDY.xcodeproj/project.pbxproj b/SPDY.xcodeproj/project.pbxproj index 053a56d..3c931a6 100644 --- a/SPDY.xcodeproj/project.pbxproj +++ b/SPDY.xcodeproj/project.pbxproj @@ -107,10 +107,15 @@ 5C5EA4731A119C950058FB64 /* SPDYMockOriginEndpointManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5EA4721A119C950058FB64 /* SPDYMockOriginEndpointManager.m */; }; 5C5EA4751A119CAB0058FB64 /* SPDYSocketTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5EA4741A119CAB0058FB64 /* SPDYSocketTest.m */; }; 5C6D809A1BC44C19003AF2E0 /* SPDYURLCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D80991BC44C19003AF2E0 /* SPDYURLCacheTest.m */; settings = {ASSET_TAGS = (); }; }; - 5C6D809E1BC45569003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D809C1BC4554D003AF2E0 /* SPDYCanonicalRequest.m */; settings = {ASSET_TAGS = (); }; }; - 5C6D809F1BC45569003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D809C1BC4554D003AF2E0 /* SPDYCanonicalRequest.m */; settings = {ASSET_TAGS = (); }; }; - 5C6D80A01BC4556A003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D809C1BC4554D003AF2E0 /* SPDYCanonicalRequest.m */; settings = {ASSET_TAGS = (); }; }; + 5C6D80AB1BC457B3003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D80A91BC457A9003AF2E0 /* SPDYCanonicalRequest.m */; settings = {ASSET_TAGS = (); }; }; + 5C6D80AC1BC457B4003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D80A91BC457A9003AF2E0 /* SPDYCanonicalRequest.m */; settings = {ASSET_TAGS = (); }; }; + 5C6D80AD1BC457B5003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D80A91BC457A9003AF2E0 /* SPDYCanonicalRequest.m */; settings = {ASSET_TAGS = (); }; }; + 5C750B501A390C7200CC0F2F /* SPDYPushStreamManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C750B4F1A390C7200CC0F2F /* SPDYPushStreamManagerTest.m */; }; 5C7E662F1B0533360037AD91 /* SPDYProtocolTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C7E662E1B0533360037AD91 /* SPDYProtocolTest.m */; }; + 5C8089701A266C5700CAC4FF /* SPDYPushStreamManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C80896D1A266C5700CAC4FF /* SPDYPushStreamManager.h */; }; + 5C8089721A266C8000CAC4FF /* SPDYPushStreamManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C8089711A266C8000CAC4FF /* SPDYPushStreamManager.m */; }; + 5C8089731A266C8000CAC4FF /* SPDYPushStreamManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C8089711A266C8000CAC4FF /* SPDYPushStreamManager.m */; }; + 5C8089741A266C8000CAC4FF /* SPDYPushStreamManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C8089711A266C8000CAC4FF /* SPDYPushStreamManager.m */; }; 5C9A0BD01A363BDC00CF2D3D /* SPDYOriginEndpointManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A0BCF1A363BDC00CF2D3D /* SPDYOriginEndpointManager.m */; }; 5C9A0BD11A363BDC00CF2D3D /* SPDYOriginEndpointManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A0BCF1A363BDC00CF2D3D /* SPDYOriginEndpointManager.m */; }; 5C9A0BD21A363BDC00CF2D3D /* SPDYOriginEndpointManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A0BCF1A363BDC00CF2D3D /* SPDYOriginEndpointManager.m */; }; @@ -198,9 +203,12 @@ 5C5EA4721A119C950058FB64 /* SPDYMockOriginEndpointManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYMockOriginEndpointManager.m; sourceTree = ""; }; 5C5EA4741A119CAB0058FB64 /* SPDYSocketTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYSocketTest.m; sourceTree = ""; }; 5C6D80991BC44C19003AF2E0 /* SPDYURLCacheTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYURLCacheTest.m; sourceTree = ""; }; - 5C6D809B1BC4554D003AF2E0 /* SPDYCanonicalRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYCanonicalRequest.h; sourceTree = ""; }; - 5C6D809C1BC4554D003AF2E0 /* SPDYCanonicalRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYCanonicalRequest.m; sourceTree = ""; }; + 5C6D80A81BC457A9003AF2E0 /* SPDYCanonicalRequest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYCanonicalRequest.h; sourceTree = ""; }; + 5C6D80A91BC457A9003AF2E0 /* SPDYCanonicalRequest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYCanonicalRequest.m; sourceTree = ""; }; + 5C750B4F1A390C7200CC0F2F /* SPDYPushStreamManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYPushStreamManagerTest.m; sourceTree = ""; }; 5C7E662E1B0533360037AD91 /* SPDYProtocolTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYProtocolTest.m; sourceTree = ""; }; + 5C80896D1A266C5700CAC4FF /* SPDYPushStreamManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYPushStreamManager.h; sourceTree = ""; }; + 5C8089711A266C8000CAC4FF /* SPDYPushStreamManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYPushStreamManager.m; sourceTree = ""; }; 5C9A0BCB1A363B7700CF2D3D /* SPDYOriginEndpointManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYOriginEndpointManager.h; sourceTree = ""; }; 5C9A0BCF1A363BDC00CF2D3D /* SPDYOriginEndpointManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYOriginEndpointManager.m; sourceTree = ""; }; 5CA0B9C71A6486F10068ABD9 /* SPDYSettingsStoreTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYSettingsStoreTest.m; sourceTree = ""; }; @@ -293,6 +301,7 @@ 5C04570419B043CB009E0AC2 /* SPDYOriginEndpointTest.m */, 0679F3CE186217FC006F122E /* SPDYOriginTest.m */, 5C7E662E1B0533360037AD91 /* SPDYProtocolTest.m */, + 5C750B4F1A390C7200CC0F2F /* SPDYPushStreamManagerTest.m */, 7774C803F0948788139CD2C1 /* SPDYServerPushTest.m */, 25959A3E1937DE3900FC9731 /* SPDYSessionManagerTest.m */, 7774C2026A4DE9957D75F629 /* SPDYSessionTest.m */, @@ -389,8 +398,8 @@ D2CC14B816179B43002E37CF /* SPDY-Prefix.pch */, 5C48CF8B1B0A68400082F7EF /* SPDYCacheStoragePolicy.h */, 5C48CF8C1B0A68400082F7EF /* SPDYCacheStoragePolicy.m */, - 5C6D809B1BC4554D003AF2E0 /* SPDYCanonicalRequest.h */, - 5C6D809C1BC4554D003AF2E0 /* SPDYCanonicalRequest.m */, + 5C6D80A81BC457A9003AF2E0 /* SPDYCanonicalRequest.h */, + 5C6D80A91BC457A9003AF2E0 /* SPDYCanonicalRequest.m */, 062EA63E175D4CD3003BC1CE /* SPDYCommonLogger.h */, 062EA63F175D4CD3003BC1CE /* SPDYCommonLogger.m */, 062EA63C175D1A15003BC1CE /* SPDYDefinitions.h */, @@ -417,6 +426,8 @@ D2CC14C01618CF62002E37CF /* SPDYProtocol.h */, D2CC14C11618CF62002E37CF /* SPDYProtocol.m */, 8B40E8A81AE1B95C000A0F0F /* SPDYProtocol+Project.h */, + 5C80896D1A266C5700CAC4FF /* SPDYPushStreamManager.h */, + 5C8089711A266C8000CAC4FF /* SPDYPushStreamManager.m */, D2CC14C31618DBF2002E37CF /* SPDYSession.h */, D2CC14C41618DBF2002E37CF /* SPDYSession.m */, D2CC14CC161A5826002E37CF /* SPDYSessionManager.h */, @@ -483,6 +494,7 @@ 8B40E8AC1AE1B95C000A0F0F /* SPDYProtocol+Project.h in Headers */, 06811C9B1715DC85000D1677 /* SPDYLogger.h in Headers */, 064A05C716F7C313008C7D08 /* SPDYProtocol.h in Headers */, + 5C8089701A266C5700CAC4FF /* SPDYPushStreamManager.h in Headers */, 06E7BF131824371F004DB65D /* SPDYTLSTrustEvaluator.h in Headers */, 062EA640175D4CD3003BC1CE /* SPDYCommonLogger.h in Headers */, 7774CF887055793F373F0D5E /* SPDYStopwatch.h in Headers */, @@ -671,6 +683,7 @@ 06FDA20B16717DF100137DBD /* SPDYFrameDecoder.m in Sources */, 0679F3CF186217FC006F122E /* SPDYOriginTest.m in Sources */, 06FDA20D16717DF100137DBD /* SPDYProtocol.m in Sources */, + 5C750B501A390C7200CC0F2F /* SPDYPushStreamManagerTest.m in Sources */, 06FDA20F16717DF100137DBD /* SPDYSession.m in Sources */, 06FDA21116717DF100137DBD /* SPDYSessionManager.m in Sources */, 5CF0A2C91A089BC500B6D141 /* SPDYMetadataTest.m in Sources */, @@ -694,6 +707,7 @@ 067EBFE717418F350029F16C /* SPDYStreamTest.m in Sources */, 062EA642175D4CD3003BC1CE /* SPDYCommonLogger.m in Sources */, 5C5EA46E1A119B630058FB64 /* SPDYOriginEndpoint.m in Sources */, + 5C6D80AB1BC457B3003AF2E0 /* SPDYCanonicalRequest.m in Sources */, 25959A3F1937DE3900FC9731 /* SPDYSessionManagerTest.m in Sources */, 5CE43CE11AD74FC900E73FAC /* SPDYMetadata+Utils.m in Sources */, 061C8E9517C5954400D22083 /* SPDYStreamManager.m in Sources */, @@ -701,8 +715,8 @@ 7774C1F1E544793907908882 /* SPDYMockFrameEncoderDelegate.m in Sources */, 7774CA1FA1F4A59CA0906BB7 /* SPDYSocket+SPDYSocketMock.m in Sources */, 7774CEA5AB5D1536D0CDCEA4 /* SPDYServerPushTest.m in Sources */, - 5C6D80A01BC4556A003AF2E0 /* SPDYCanonicalRequest.m in Sources */, 7774C1318AB029C6BCEF84D6 /* SPDYSessionTest.m in Sources */, + 5C8089721A266C8000CAC4FF /* SPDYPushStreamManager.m in Sources */, 5C48CF901B0A684D0082F7EF /* SPDYCacheStoragePolicy.m in Sources */, 5C7E662F1B0533360037AD91 /* SPDYProtocolTest.m in Sources */, 7774C2FBEA6973E90CAE7005 /* SPDYMockSessionTestBase.m in Sources */, @@ -718,11 +732,13 @@ 0651EC3C16F3FA1400CE44D2 /* NSURLRequest+SPDYURLRequest.m in Sources */, 5C5EA46F1A119B630058FB64 /* SPDYOriginEndpoint.m in Sources */, 0651EC3D16F3FA1400CE44D2 /* SPDYFrame.m in Sources */, + 5C8089731A266C8000CAC4FF /* SPDYPushStreamManager.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 */, + 5C6D80AD1BC457B5003AF2E0 /* SPDYCanonicalRequest.m in Sources */, 0651EC4316F3FA1400CE44D2 /* SPDYSession.m in Sources */, 0651EC4416F3FA1400CE44D2 /* SPDYSessionManager.m in Sources */, 0651EC4516F3FA1400CE44D2 /* SPDYSettingsStore.m in Sources */, @@ -731,7 +747,6 @@ 062EA643175D4CD3003BC1CE /* SPDYCommonLogger.m in Sources */, 5CE43CE21AD74FC900E73FAC /* SPDYMetadata+Utils.m in Sources */, 061C8E9617C5954400D22083 /* SPDYStreamManager.m in Sources */, - 5C6D809E1BC45569003AF2E0 /* SPDYCanonicalRequest.m in Sources */, 5C210A0B1A5F48C500ADB538 /* SPDYSessionPool.m in Sources */, 5C48CF8E1B0A684C0082F7EF /* SPDYCacheStoragePolicy.m in Sources */, 06B290CF1861018A00540A03 /* SPDYOrigin.m in Sources */, @@ -748,11 +763,13 @@ 0651EC2216F3FA0B00CE44D2 /* NSURLRequest+SPDYURLRequest.m in Sources */, 5C5EA4701A119B630058FB64 /* SPDYOriginEndpoint.m in Sources */, 0651EC2316F3FA0B00CE44D2 /* SPDYFrame.m in Sources */, + 5C8089741A266C8000CAC4FF /* SPDYPushStreamManager.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 */, + 5C6D80AC1BC457B4003AF2E0 /* SPDYCanonicalRequest.m in Sources */, 0651EC2916F3FA0B00CE44D2 /* SPDYSession.m in Sources */, 0651EC2A16F3FA0B00CE44D2 /* SPDYSessionManager.m in Sources */, 0651EC2B16F3FA0B00CE44D2 /* SPDYSettingsStore.m in Sources */, @@ -761,7 +778,6 @@ 062EA645175D4CD3003BC1CE /* SPDYCommonLogger.m in Sources */, 5CE43CE31AD74FCA00E73FAC /* SPDYMetadata+Utils.m in Sources */, 061C8E9817C5954400D22083 /* SPDYStreamManager.m in Sources */, - 5C6D809F1BC45569003AF2E0 /* SPDYCanonicalRequest.m in Sources */, 5C210A0C1A5F48C500ADB538 /* SPDYSessionPool.m in Sources */, 5C48CF8F1B0A684C0082F7EF /* SPDYCacheStoragePolicy.m in Sources */, 06B290D21861018A00540A03 /* SPDYOrigin.m in Sources */, diff --git a/SPDY/SPDYProtocol.m b/SPDY/SPDYProtocol.m index b7884ad..90fb4d0 100644 --- a/SPDY/SPDYProtocol.m +++ b/SPDY/SPDYProtocol.m @@ -21,6 +21,7 @@ #import "SPDYMetadata+Utils.h" #import "SPDYOrigin.h" #import "SPDYProtocol+Project.h" +#import "SPDYPushStreamManager.h" #import "SPDYSession.h" #import "SPDYSessionManager.h" #import "SPDYStream.h" @@ -382,16 +383,14 @@ - (void)startLoading origin = aliasedOrigin; } - // Create the stream - _stream = [[SPDYStream alloc] initWithProtocol:self]; + // Create the context, but delay stream creation (to allow for looking up push cache + // as late as possible) _context = [[SPDYProtocolContext alloc] initWithStream:_stream]; if (request.SPDYURLSession) { [self detectSessionAndTaskThenContinueWithOrigin:origin]; } else { - // Start the stream - SPDYSessionManager *manager = [SPDYSessionManager localManagerForOrigin:origin]; - [manager queueStream:_stream]; + [self startStreamForOrigin:origin]; } } @@ -435,9 +434,7 @@ - (void)detectSessionAndTaskThenContinueWithOrigin:(SPDYOrigin *)origin } } - // Start the stream - SPDYSessionManager *manager = [SPDYSessionManager localManagerForOrigin:origin]; - [manager queueStream:_stream]; + [self startStreamForOrigin:origin]; } }; @@ -447,10 +444,26 @@ - (void)detectSessionAndTaskThenContinueWithOrigin:(SPDYOrigin *)origin }]; } +- (void)startStreamForOrigin:(SPDYOrigin *)origin +{ + SPDYSessionManager *manager = [SPDYSessionManager localManagerForOrigin:origin]; + + // See if this is currently being pushed, and if so, hook it up, else create it + _stream = [manager.pushStreamManager streamForProtocol:self]; + if (_stream != nil) { + SPDY_INFO(@"using in-progress push stream %@ for %@", _stream, self.request.URL.absoluteString); + } else { + _stream = [[SPDYStream alloc] initWithProtocol:self pushStreamManager:manager.pushStreamManager]; + [manager queueStream:_stream]; + } +} + - (void)stopLoading { SPDY_INFO(@"stop loading %@", self.request.URL.absoluteString); + [_stream.pushStreamManager stopLoadingStream:_stream]; + if (_stream && !_stream.closed) { [_stream cancel]; } diff --git a/SPDY/SPDYPushStreamManager.h b/SPDY/SPDYPushStreamManager.h new file mode 100644 index 0000000..54cbf16 --- /dev/null +++ b/SPDY/SPDYPushStreamManager.h @@ -0,0 +1,25 @@ +// +// SPDYPushStreamManager.h +// SPDY +// +// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Kevin Goodier. +// + +#import + +@class SPDYProtocol; +@class SPDYStream; + +@interface SPDYPushStreamManager : NSObject + +- (NSUInteger)pushStreamCount; +- (NSUInteger)associatedStreamCount; +- (SPDYStream *)streamForProtocol:(SPDYProtocol *)protocol; +- (void)addStream:(SPDYStream *)stream associatedWith:(SPDYStream *)associatedStream; +- (void)stopLoadingStream:(SPDYStream *)stream; + +@end diff --git a/SPDY/SPDYPushStreamManager.m b/SPDY/SPDYPushStreamManager.m new file mode 100644 index 0000000..bd0e6b7 --- /dev/null +++ b/SPDY/SPDYPushStreamManager.m @@ -0,0 +1,296 @@ +// +// SPDYPushStreamManager.m +// SPDY +// +// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Kevin Goodier. +// + +#import +#import +#import "SPDYCommonLogger.h" +#import "SPDYPushStreamManager.h" +#import "SPDYProtocol.h" +#import "SPDYStream.h" + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +@interface SPDYPushStreamManager () +- (void)removeStream:(SPDYStream *)stream; +@end + +@interface SPDYPushStreamNode : NSObject +@property (nonatomic, readonly) SPDYStream *stream; +@property (nonatomic, readonly) SPDYStream *associatedStream; +- (id)initWithStream:(SPDYStream *)stream associatedStream:(SPDYStream *)associatedStream; +- (SPDYStream *)attachStreamToProtocol:(SPDYProtocol *)protocol; +@end + +@implementation SPDYPushStreamNode +{ + NSURLResponse *_response; + NSURLCacheStoragePolicy _cacheStoragePolicy; + NSMutableData *_data; + NSError *_error; + BOOL _done; +} + +- (id)initWithStream:(SPDYStream *)stream associatedStream:(SPDYStream *)associatedStream; +{ + self = [super init]; + if (self) { + _stream = stream; + _associatedStream = associatedStream; + _data = [[NSMutableData alloc] init]; + + // Take ownership of callbacks + _stream.client = self; + } + return self; +} + +- (SPDYStream *)attachStreamToProtocol:(SPDYProtocol *)protocol +{ + if (protocol.client == nil) { + SPDY_ERROR(@"PUSH.%u: can't attach stream to protocol with nil client", _stream.streamId); + return nil; + } + + _stream.protocol = protocol; + _stream.client = protocol.client; + + // @@@ Compare protocol.request with _stream.request? + + // Play "catch up" on missed callbacks + + if (_response) { + SPDY_DEBUG(@"PUSH.%u: replaying didReceiveResponse: %@", _stream.streamId, _response); + [protocol.client URLProtocol:protocol didReceiveResponse:_response cacheStoragePolicy:_cacheStoragePolicy]; + } + + if (_data.length > 0) { + SPDY_DEBUG(@"PUSH.%u: replaying didLoadData: %zd bytes", _stream.streamId, _data.length); + [protocol.client URLProtocol:protocol didLoadData:_data]; + } + + if (_error) { + SPDY_DEBUG(@"PUSH.%u: replaying didFailWithError: %@", _stream.streamId, _error); + [protocol.client URLProtocol:protocol didFailWithError:_error]; + } else if (_done) { + SPDY_DEBUG(@"PUSH.%u: replaying didFinishLoading", _stream.streamId); + [protocol.client URLProtocolDidFinishLoading:protocol]; + } + + return _stream; +} + +#pragma mark URLProtocolClient overrides + +// Note: protocol will be nil for all of these. + +- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveResponse:(NSURLResponse *)response cacheStoragePolicy:(NSURLCacheStoragePolicy)policy +{ + SPDY_DEBUG(@"PUSH.%u: internal URLProtocol received response %@, cache policy %zd", _stream.streamId, response, policy); + _response = response; + _cacheStoragePolicy = policy; +} + +- (void)URLProtocol:(NSURLProtocol *)protocol didLoadData:(NSData *)data +{ + SPDY_DEBUG(@"PUSH.%u: internal URLProtocol loaded %zd data bytes", _stream.streamId, data.length); + [_data appendData:data]; +} + +- (void)URLProtocolDidFinishLoading:(NSURLProtocol *)protocol +{ + SPDY_DEBUG(@"PUSH.%u: internal URLProtocol finished", _stream.streamId); + _done = YES; + // @@@ TODO: cache it in NSURLCache per _cacheStoragePolicy? + [_stream.pushStreamManager stopLoadingStream:_stream]; +} + +- (void)URLProtocol:(NSURLProtocol *)protocol didFailWithError:(NSError *)error +{ + SPDY_DEBUG(@"PUSH.%u: internal URLProtocol failed with error: %@", _stream.streamId, error); + _error = error; + [_stream.pushStreamManager removeStream:_stream]; +} + +- (void)URLProtocol:(NSURLProtocol *)protocol wasRedirectedToRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse +{ + NSAssert(false, @"not supported for push requests"); +} + +- (void)URLProtocol:(NSURLProtocol *)protocol cachedResponseIsValid:(NSCachedURLResponse *)cachedResponse +{ + NSAssert(false, @"not supported for push requests"); +} + +- (void)URLProtocol:(NSURLProtocol *)protocol didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge +{ + NSAssert(false, @"not supported for push requests"); +} + +- (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge +{ + NSAssert(false, @"not supported for push requests"); +} + +@end + + +@implementation SPDYPushStreamManager +{ + NSMapTable *_streamToNodeDictionary; + NSMapTable *_urlToNodeDictionary; + NSMapTable *_associatedStreamToNodeArrayDictionary; +} + +- (id)init +{ + self = [super init]; + if (self) { + _streamToNodeDictionary = [NSMapTable strongToStrongObjectsMapTable]; + _urlToNodeDictionary = [NSMapTable strongToStrongObjectsMapTable]; + _associatedStreamToNodeArrayDictionary = [NSMapTable strongToStrongObjectsMapTable]; + } + return self; +} + +- (NSUInteger)pushStreamCount +{ + return _streamToNodeDictionary.count; +} + +- (NSUInteger)associatedStreamCount +{ + return _associatedStreamToNodeArrayDictionary.count; +} + +- (SPDYStream *)streamForProtocol:(SPDYProtocol *)protocol +{ + // @@@ This lookup in our "cache" is only based on the URL. Is there more we need to take + // into account? + NSURL *requestURL = protocol.request.URL; // protocol.request has already been canonicalized + SPDYPushStreamNode *node = [_urlToNodeDictionary objectForKey:requestURL]; + + if (node == nil) { + return nil; + } + + [self removeStream:node.stream]; + + SPDY_DEBUG(@"PUSH.%u: attaching push stream to protocol for request %@", node.stream.streamId, requestURL); + return [node attachStreamToProtocol:protocol]; +} + +- (void)addStream:(SPDYStream *)stream associatedWith:(SPDYStream *)associatedStream +{ + SPDY_INFO(@"PUSH.%u: adding stream (%@) associated with stream %u (%@)", stream.streamId, stream.request.URL, associatedStream.streamId, associatedStream.request.URL); + + // We're taking ownership of this stream + NSAssert(!stream.local, @"must be a push stream"); + NSAssert(stream.client == nil, @"push streams must have no owner"); + + SPDYPushStreamNode *node = [[SPDYPushStreamNode alloc] initWithStream:stream associatedStream:associatedStream]; + + // In the event a stream with matching request already exists, the new one wins. + [_streamToNodeDictionary setObject:node forKey:stream]; + + // Add mapping from original request to push requests. Allows us to cancel all push + // requests if the original request is cancelled. + if (associatedStream) { + NSAssert(associatedStream.local, @"associated stream must be local"); + if ([_associatedStreamToNodeArrayDictionary objectForKey:associatedStream] == nil) { + [_associatedStreamToNodeArrayDictionary setObject:[[NSMutableArray alloc] initWithObjects:node, nil] forKey:associatedStream]; + } else { + [[_associatedStreamToNodeArrayDictionary objectForKey:associatedStream] addObject:node]; + } + } + + // Add mapping from URL to node to provide cache lookups + NSAssert(stream.request, @"push stream must have a request object"); + [_urlToNodeDictionary setObject:node forKey:stream.request.URL]; +} + +- (void)stopLoadingStream:(SPDYStream *)stream +{ + // Various conditions considered here for 'stream': + // [local, open] Remove stream (it's being cancelled). All related remote streams will also be cancelled and removed. + // [local, closed] Remove stream. All related closed remote streams will be removed. + // [remote, open] Remove stream (it's being cancelled). + // [remote, closed] Remove stream only if its associated local stream has been removed, or if it failed. + if (stream.local) { + // Make copy because removeStream will mutate the underlying array + NSArray *pushNodes = [[_associatedStreamToNodeArrayDictionary objectForKey:stream] copy]; + if (pushNodes.count > 0) { + for (SPDYPushStreamNode *pushNode in pushNodes) { + if (!stream.closed) { + SPDY_DEBUG(@"PUSH.%u: stopping local stream, cancelling pushed stream %u", stream.streamId, pushNode.stream.streamId); + [pushNode.stream cancel]; + [self removeStream:pushNode.stream]; + } else if (pushNode.stream.closed) { + SPDY_DEBUG(@"PUSH.%u: stopping local stream, removing pushed stream %u", stream.streamId, pushNode.stream.streamId); + [self removeStream:pushNode.stream]; + } else { + // else open push streams are left alone until they finish + SPDY_DEBUG(@"PUSH.%u: stopping local stream, leaving pushed stream %u", stream.streamId, pushNode.stream.streamId); + } + } + } + [self removeStream:stream]; + } else { + // We only remove a pushed stream that is stopping when it has no associated stream. + // If it does have one, then we leave it here in the in-memory cache until either + // a new request attaches to it (see streamForProtocol), or the associated stream stops + // (see stopLoadingStream for the local stream case when the pushed stream is closed). + // + // TODO: this is where we should insert the response into a NSURLCache and remove it + // from the in-memory cache here, for both cases. In particular, leaving the push stream + // around while the associated stream is open could lead to leaks if the app never + // issues requests that hook up to the pushed streams. + SPDYStream *associatedStream = stream.associatedStream; // get strong reference + BOOL hasAssociatedStream = (associatedStream && [_associatedStreamToNodeArrayDictionary objectForKey:associatedStream]); + if (!hasAssociatedStream) { + SPDY_DEBUG(@"PUSH.%u: removing pushed stream", stream.streamId); + [self removeStream:stream]; + } else { + SPDY_DEBUG(@"PUSH.%u: leaving pushed stream with associated stream %u", stream.streamId, stream.associatedStream.streamId); + } + } +} + +- (void)removeStream:(SPDYStream *)stream +{ + if (stream == nil) { + return; + } + + if (stream.local) { + [_associatedStreamToNodeArrayDictionary removeObjectForKey:stream]; + } else { + SPDYPushStreamNode *pushNode = [_streamToNodeDictionary objectForKey:stream]; + [_streamToNodeDictionary removeObjectForKey:stream]; + if (stream.request != nil) { + [_urlToNodeDictionary removeObjectForKey:stream.request.URL]; + } + + // Remove the stream from the list of streams related to associated (original) stream. + NSAssert(pushNode.associatedStream, @"push stream must have associated stream"); + NSMutableArray *associatedNodes = [_associatedStreamToNodeArrayDictionary objectForKey:pushNode.associatedStream]; + for (NSUInteger i = 0; i < associatedNodes.count; i++) { + SPDYPushStreamNode *node = associatedNodes[i]; + if (node.stream == stream) { + [associatedNodes removeObjectAtIndex:i]; + break; + } + } + } +} + +@end diff --git a/SPDY/SPDYSessionManager.h b/SPDY/SPDYSessionManager.h index f68ef62..8067d09 100644 --- a/SPDY/SPDYSessionManager.h +++ b/SPDY/SPDYSessionManager.h @@ -11,8 +11,12 @@ #import +@class SPDYPushStreamManager; + @interface SPDYSessionManager : NSObject +@property (nonatomic, readonly) SPDYPushStreamManager *pushStreamManager; + + (SPDYSessionManager *)localManagerForOrigin:(SPDYOrigin *)origin; - (void)queueStream:(SPDYStream *)stream; diff --git a/SPDY/SPDYSessionManager.m b/SPDY/SPDYSessionManager.m index a32032f..b763b42 100644 --- a/SPDY/SPDYSessionManager.m +++ b/SPDY/SPDYSessionManager.m @@ -19,6 +19,7 @@ #import "SPDYCommonLogger.h" #import "SPDYOrigin.h" #import "SPDYProtocol.h" +#import "SPDYPushStreamManager.h" #import "SPDYSession.h" #import "SPDYSessionManager.h" #import "SPDYSessionPool.h" @@ -74,6 +75,7 @@ - (instancetype)initWithOrigin:(SPDYOrigin *)origin { self = [super init]; if (self) { + _pushStreamManager = [[SPDYPushStreamManager alloc] init]; _origin = origin; _pendingStreams = [[SPDYStreamManager alloc] init]; _basePool = [[SPDYSessionPool alloc] init]; diff --git a/SPDY/SPDYStream.h b/SPDY/SPDYStream.h index 7c2c7f7..28004a5 100644 --- a/SPDY/SPDYStream.h +++ b/SPDY/SPDYStream.h @@ -13,6 +13,7 @@ #import "SPDYDefinitions.h" @class SPDYProtocol; +@class SPDYPushStreamManager; @class SPDYMetadata; @class SPDYStream; @@ -33,6 +34,7 @@ @property (nonatomic) NSDictionary *headers; @property (nonatomic, weak) NSURLRequest *request; @property (nonatomic, weak) SPDYProtocol *protocol; +@property (nonatomic, weak) SPDYPushStreamManager *pushStreamManager; @property (nonatomic, weak) SPDYStream *associatedStream; @property (nonatomic) SPDYStreamId streamId; @property (nonatomic) uint8_t priority; @@ -48,11 +50,9 @@ @property (nonatomic) uint32_t sendWindowSizeLowerBound; @property (nonatomic) uint32_t receiveWindowSizeLowerBound; -- (instancetype)initWithProtocol:(SPDYProtocol *)protocol; -- (instancetype)initWithAssociatedStream:(SPDYStream *)associatedStream - priority:(uint8_t)priority; -- (void)startWithStreamId:(SPDYStreamId)id sendWindowSize:(uint32_t)sendWindowSize - receiveWindowSize:(uint32_t)receiveWindowSize; +- (instancetype)initWithProtocol:(SPDYProtocol *)protocol pushStreamManager:(SPDYPushStreamManager *)pushStreamManager; +- (instancetype)initWithAssociatedStream:(SPDYStream *)associatedStream priority:(uint8_t)priority; +- (void)startWithStreamId:(SPDYStreamId)id sendWindowSize:(uint32_t)sendWindowSize receiveWindowSize:(uint32_t)receiveWindowSize; - (bool)reset; - (NSData *)readData:(NSUInteger)length error:(NSError **)pError; - (void)cancel; diff --git a/SPDY/SPDYStream.m b/SPDY/SPDYStream.m index ce38a65..4f1c6b0 100644 --- a/SPDY/SPDYStream.m +++ b/SPDY/SPDYStream.m @@ -17,10 +17,12 @@ #import #import "NSURLRequest+SPDYURLRequest.h" #import "SPDYCacheStoragePolicy.h" +#import "SPDYCanonicalRequest.h" #import "SPDYCommonLogger.h" #import "SPDYDefinitions.h" #import "SPDYMetadata+Utils.h" #import "SPDYProtocol+Project.h" +#import "SPDYPushStreamManager.h" #import "SPDYStopwatch.h" #import "SPDYStream.h" @@ -72,10 +74,12 @@ @implementation SPDYStream } - (instancetype)initWithProtocol:(SPDYProtocol *)protocol + pushStreamManager:(SPDYPushStreamManager *)pushStreamManager { self = [super init]; if (self) { _protocol = protocol; + _pushStreamManager = pushStreamManager; _client = protocol.client; _request = protocol.request; _priority = (uint8_t)MIN(_request.SPDYPriority, 0x07); @@ -100,6 +104,7 @@ - (id)initWithAssociatedStream:(SPDYStream *)associatedStream priority:(uint8_t) self = [super init]; if (self) { _protocol = nil; + _pushStreamManager = associatedStream.pushStreamManager; _client = nil; _request = nil; _priority = priority; @@ -517,6 +522,7 @@ - (void)didReceiveResponse } } + // Check encoding, but only do it once, so look at newHeaders NSString *encoding = allHTTPHeaders[@"content-encoding"]; _compressedResponse = [encoding hasPrefix:@"deflate"] || [encoding hasPrefix:@"gzip"]; if (_compressedResponse) { @@ -620,6 +626,8 @@ - (void)didReceivePushRequest _pushRequest = [SPDYProtocol canonicalRequestForRequest:requestCopy]; _request = _pushRequest; // need a strong reference for _request's weak one + + [_pushStreamManager addStream:self associatedWith:_associatedStream]; } - (void)didLoadData:(NSData *)data diff --git a/SPDYUnitTests/SPDYMockSessionTestBase.h b/SPDYUnitTests/SPDYMockSessionTestBase.h index 8f85e8d..9da24af 100644 --- a/SPDYUnitTests/SPDYMockSessionTestBase.h +++ b/SPDYUnitTests/SPDYMockSessionTestBase.h @@ -53,6 +53,10 @@ typedef void (^SPDYAsyncTestCallback)(); SPDYMockFrameDecoderDelegate *_mockDecoderDelegate; SPDYMockURLProtocolClient *_mockURLProtocolClient; SPDYMockStreamDelegate *_mockStreamDelegate; + + SPDYPushStreamManager *_pushStreamManager; + NSMutableArray *_pushProtocolList; + SPDYMockURLProtocolClient *_mockPushURLProtocolClient; } - (void)setUp; @@ -61,6 +65,7 @@ typedef void (^SPDYAsyncTestCallback)(); - (SPDYProtocol *)createProtocol; - (SPDYStream *)createStream; - (void)makeSessionReadData:(NSData *)data; +- (SPDYStream *)attachToPushRequestWithUrl:(NSString *)url; - (SPDYStream *)mockSynStreamAndReplyWithId:(SPDYStreamId)streamId last:(bool)last; - (void)mockServerSynReplyWithId:(SPDYStreamId)streamId last:(BOOL)last; diff --git a/SPDYUnitTests/SPDYMockSessionTestBase.m b/SPDYUnitTests/SPDYMockSessionTestBase.m index c56ff1e..5d4451e 100644 --- a/SPDYUnitTests/SPDYMockSessionTestBase.m +++ b/SPDYUnitTests/SPDYMockSessionTestBase.m @@ -15,6 +15,7 @@ #import "SPDYMockSessionTestBase.h" #import "SPDYMockURLProtocolClient.h" #import "SPDYOrigin.h" +#import "SPDYPushStreamManager.h" #import "SPDYSocket.h" #import "SPDYSocket+SPDYSocketMock.h" #import "SPDYStream.h" @@ -70,6 +71,8 @@ - (void)setUp { [super setUp]; [SPDYSocket performSwizzling:YES]; _protocolList = [[NSMutableArray alloc] initWithCapacity:1]; + _pushProtocolList = [[NSMutableArray alloc] initWithCapacity:1]; + _pushStreamManager = [[SPDYPushStreamManager alloc] init]; NSError *error = nil; _origin = [[SPDYOrigin alloc] initWithString:@"http://mocked" error:&error]; @@ -105,7 +108,7 @@ - (SPDYProtocol *)createProtocol - (SPDYStream *)createStream { - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol]]; + SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol] pushStreamManager:_pushStreamManager]; stream.delegate = _mockStreamDelegate; return stream; } @@ -124,6 +127,17 @@ - (void)makeSocketConnect [[_session socket] performDelegateCall_socketDidConnectToHost:@"testhost" port:1234]; } +- (SPDYStream *)attachToPushRequestWithUrl:(NSString *)url +{ + _mockPushURLProtocolClient = [[SPDYMockURLProtocolClient alloc] init]; + NSMutableURLRequest *pushURLRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; + SPDYProtocol *pushProtocolRequest = [[SPDYProtocol alloc] initWithRequest:pushURLRequest cachedResponse:nil client:_mockPushURLProtocolClient]; + [_pushProtocolList addObject:pushProtocolRequest]; + + SPDYStream *pushStream = [_pushStreamManager streamForProtocol:pushProtocolRequest]; + return pushStream; +} + - (SPDYStream *)mockSynStreamAndReplyWithId:(SPDYStreamId)streamId last:(bool)last { if (streamId == 1) { diff --git a/SPDYUnitTests/SPDYPushStreamManagerTest.m b/SPDYUnitTests/SPDYPushStreamManagerTest.m new file mode 100644 index 0000000..1e39671 --- /dev/null +++ b/SPDYUnitTests/SPDYPushStreamManagerTest.m @@ -0,0 +1,207 @@ +// +// SPDYPushStreamManagerTest.m +// SPDY +// +// Copyright (c) 2014 Twitter, Inc. All rights reserved. +// Licensed under the Apache License v2.0 +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Created by Kevin Goodier. +// + +#import +#import "SPDYMockSessionTestBase.h" +#import "SPDYPushStreamManager.h" +#import "SPDYMockURLProtocolClient.h" + +@interface SPDYPushStreamManagerTest : SPDYMockSessionTestBase +@end + +@implementation SPDYPushStreamManagerTest +{ + SPDYStream *_associatedStream; + SPDYStream *_pushStream1; + SPDYStream *_pushStream2; +} + +- (void)setUp +{ + [super setUp]; + _associatedStream = nil; + _pushStream1 = nil; + _pushStream2 = nil; +} + +- (void)_addTwoPushStreams +{ + _URLRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://mocked/init"]]; + _associatedStream = [self createStream]; + _associatedStream.streamId = 1; + + _URLRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://mocked/pushed"]]; + _pushStream1 = [self createStream]; + _pushStream1.streamId = 2; + _pushStream1.local = NO; + _pushStream1.client = nil; + _pushStream1.associatedStream = _associatedStream; + + _URLRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://mocked/pushed2"]]; + _pushStream2 = [self createStream]; + _pushStream2.streamId = 4; + _pushStream2.local = NO; + _pushStream2.client = nil; + _pushStream2.associatedStream = _associatedStream; + + [_pushStreamManager addStream:_pushStream1 associatedWith:_associatedStream]; + [_pushStreamManager addStream:_pushStream2 associatedWith:_associatedStream]; + + XCTAssertEqual(_pushStreamManager.pushStreamCount, 2U); + XCTAssertEqual(_pushStreamManager.associatedStreamCount, 1U); +} + +- (void)testStreamForProtocolNotFound +{ + SPDYMockURLProtocolClient *mockPushURLProtocolClient = [[SPDYMockURLProtocolClient alloc] init]; + NSMutableURLRequest *pushURLRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://mocked/notfound"]]; + SPDYProtocol *pushProtocolRequest = [[SPDYProtocol alloc] initWithRequest:pushURLRequest cachedResponse:nil client:mockPushURLProtocolClient]; + + SPDYStream *pushStream = [_pushStreamManager streamForProtocol:pushProtocolRequest]; + XCTAssertNil(pushStream); +} + +- (void)testAttachToPushRequestWillAttachToPushStream +{ + [self _addTwoPushStreams]; + + SPDYStream *pushStream = [self attachToPushRequestWithUrl:@"http://mocked/pushed"]; + XCTAssertNotNil(pushStream); + XCTAssertEqualObjects(pushStream.request.URL.absoluteString, @"http://mocked/pushed"); + XCTAssertEqual(_pushStreamManager.pushStreamCount, 1U); + XCTAssertEqual(_pushStreamManager.associatedStreamCount, 1U); + + pushStream = [self attachToPushRequestWithUrl:@"http://mocked/pushed2"]; + XCTAssertNotNil(pushStream); + XCTAssertEqualObjects(pushStream.request.URL.absoluteString, @"http://mocked/pushed2"); + XCTAssertEqual(_pushStreamManager.pushStreamCount, 0U); + XCTAssertEqual(_pushStreamManager.associatedStreamCount, 1U); +} + +- (void)testAttachToPushRequestWillNotAttachToAssociatedStream +{ + [self _addTwoPushStreams]; + + SPDYStream *pushStream = [self attachToPushRequestWithUrl:@"http://mocked/init"]; + XCTAssertNil(pushStream); + XCTAssertEqual(_pushStreamManager.pushStreamCount, 2U); + XCTAssertEqual(_pushStreamManager.associatedStreamCount, 1U); +} + +- (void)testAttachToPushRequestWillAttachToPushStreamAfterStopLoadingPushStream +{ + [self _addTwoPushStreams]; + + // Since the associated stream is still alive, this push stream will live on + [_pushStreamManager stopLoadingStream:_pushStream1]; + XCTAssertEqual(_pushStreamManager.pushStreamCount, 2U); + XCTAssertEqual(_pushStreamManager.associatedStreamCount, 1U); + + SPDYStream *pushStream = [self attachToPushRequestWithUrl:@"http://mocked/pushed"]; + XCTAssertNotNil(pushStream); + XCTAssertEqual(_pushStreamManager.pushStreamCount, 1U); + + pushStream = [self attachToPushRequestWithUrl:@"http://mocked/pushed2"]; + XCTAssertNotNil(pushStream); + XCTAssertEqualObjects(pushStream.request.URL.absoluteString, @"http://mocked/pushed2"); + XCTAssertEqual(_pushStreamManager.pushStreamCount, 0U); + XCTAssertEqual(_pushStreamManager.associatedStreamCount, 1U); + + [_pushStreamManager stopLoadingStream:_associatedStream]; + XCTAssertEqual(_pushStreamManager.associatedStreamCount, 0U); +} + +- (void)testAttachToPushRequestWillNotAttachToPushStreamAfterStopLoadingAssociatedStream +{ + [self _addTwoPushStreams]; + + [_pushStreamManager stopLoadingStream:_associatedStream]; + XCTAssertEqual(_pushStreamManager.pushStreamCount, 0U); + XCTAssertEqual(_pushStreamManager.associatedStreamCount, 0U); +} + +- (void)testAttachToPushRequestDoesMakeAllCallbacks +{ + [self _addTwoPushStreams]; + + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://mocked/pushed"] statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:@{}]; + NSMutableData *data = [NSMutableData dataWithLength:100]; + + [_pushStream1.client URLProtocol:_pushStream1.client didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed]; + [_pushStream1.client URLProtocol:_pushStream1.client didLoadData:data]; + [_pushStream1.client URLProtocol:_pushStream1.client didLoadData:data]; + [_pushStream1.client URLProtocolDidFinishLoading:_pushStream1.client]; + + SPDYStream *pushStream = [self attachToPushRequestWithUrl:@"http://mocked/pushed"]; + XCTAssertNotNil(pushStream); + XCTAssertEqualObjects(pushStream.request.URL.absoluteString, @"http://mocked/pushed"); + + SPDYMockURLProtocolClient *client = pushStream.client; + XCTAssertTrue(client.calledDidReceiveResponse); + XCTAssertTrue(client.calledDidLoadData); + XCTAssertFalse(client.calledDidFailWithError); + XCTAssertTrue(client.calledDidFinishLoading); + XCTAssertNotNil(client.lastResponse); + XCTAssertEqual(client.lastResponse.statusCode, 200); + XCTAssertEqual(client.lastCacheStoragePolicy, NSURLCacheStorageAllowed); + XCTAssertEqual(client.lastData.length, 200U); +} + +- (void)testAttachToPushRequestFailsAfterStreamFails +{ + [self _addTwoPushStreams]; + + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://mocked/pushed"] statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:@{}]; + NSMutableData *data = [NSMutableData dataWithLength:100]; + NSError *error = [NSError errorWithDomain:@"test" code:1 userInfo:nil]; + + [_pushStream1.client URLProtocol:_pushStream1.client didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed]; + [_pushStream1.client URLProtocol:_pushStream1.client didLoadData:data]; + [_pushStream1.client URLProtocol:_pushStream1.client didLoadData:data]; + [_pushStream1.client URLProtocol:_pushStream1.client didFailWithError:error]; + + SPDYStream *pushStream = [self attachToPushRequestWithUrl:@"http://mocked/pushed"]; + XCTAssertNil(pushStream); +} + +- (void)testAttachToPushRequestInMiddleDoesMakeAllCallbacks +{ + [self _addTwoPushStreams]; + + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:@"http://mocked/pushed"] statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:@{}]; + NSMutableData *data = [NSMutableData dataWithLength:100]; + + [_pushStream1.client URLProtocol:_pushStream1.client didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed]; + [_pushStream1.client URLProtocol:_pushStream1.client didLoadData:data]; + + SPDYStream *pushStream = [self attachToPushRequestWithUrl:@"http://mocked/pushed"]; + XCTAssertNotNil(pushStream); + XCTAssertEqualObjects(pushStream.request.URL.absoluteString, @"http://mocked/pushed"); + + SPDYMockURLProtocolClient *client = pushStream.client; + XCTAssertTrue(client.calledDidReceiveResponse); + XCTAssertTrue(client.calledDidLoadData); + XCTAssertFalse(client.calledDidFailWithError); + XCTAssertFalse(client.calledDidFinishLoading); + XCTAssertNotNil(client.lastResponse); + XCTAssertEqual(client.lastData.length, 100U); + + // More data then finish + data = [NSMutableData dataWithLength:50]; + [_pushStream1.client URLProtocol:_pushStream1.client didLoadData:data]; + [_pushStream1.client URLProtocolDidFinishLoading:_pushStream1.client]; + + XCTAssertTrue(client.calledDidFinishLoading); + XCTAssertEqual(client.lastData.length, 50U); // not an accumulator +} + +@end + diff --git a/SPDYUnitTests/SPDYServerPushTest.m b/SPDYUnitTests/SPDYServerPushTest.m index 672b1ef..e79d0f3 100644 --- a/SPDYUnitTests/SPDYServerPushTest.m +++ b/SPDYUnitTests/SPDYServerPushTest.m @@ -20,8 +20,9 @@ #import "SPDYMockFrameEncoderDelegate.h" #import "SPDYMockFrameDecoderDelegate.h" #import "SPDYMockSessionTestBase.h" +#import "SPDYMockURLProtocolClient.h" -@interface SPDYServerPushTest : SPDYMockSessionTestBase +@interface SPDYServerPushTest : SPDYMockSessionTestBase @end @implementation SPDYServerPushTest @@ -156,6 +157,27 @@ - (void)testSYNStreamAndAHeadersFrameWithDuplicatesRespondsWithReset #pragma mark Simple push callback tests +- (void)testSYNStreamWithStreamIDNonZeroMakesResponseCallback +{ + // Exchange initial SYN_STREAM and SYN_REPLY + [self mockSynStreamAndReplyWithId:1 last:NO]; + + // Send SYN_STREAM from server to client + [self mockServerSynStreamWithId:2 last:NO]; + [self mockServerHeadersFrameForPushWithId:2 last:NO]; + + SPDYMockURLProtocolClient *pushClient = [self attachToPushRequestWithUrl:@"http://mocked/pushed"].client; + XCTAssertTrue(pushClient.calledDidReceiveResponse); + XCTAssertFalse(pushClient.calledDidLoadData); + XCTAssertFalse(pushClient.calledDidFailWithError); + XCTAssertFalse(pushClient.calledDidFinishLoading); + + NSHTTPURLResponse *pushResponse = pushClient.lastResponse; + XCTAssertEqualObjects(pushResponse.URL.absoluteString, @"http://mocked/pushed"); + XCTAssertEqual(pushResponse.statusCode, 200); + XCTAssertEqualObjects([pushResponse.allHeaderFields valueForKey:@"PushHeader"], @"PushValue"); +} + - (void)testSYNStreamAfterAssociatedStreamClosesRespondsWithGoAway { // Exchange initial SYN_STREAM and SYN_REPLY @@ -171,4 +193,419 @@ - (void)testSYNStreamAfterAssociatedStreamClosesRespondsWithGoAway XCTAssertTrue([_mockDecoderDelegate.lastFrame isKindOfClass:[SPDYGoAwayFrame class]]); } +- (void)testSYNStreamsAndAssociatedStreamClosingDidCompleteWithMetadata +{ + [self mockSynStreamAndReplyWithId:1 last:NO]; + [self mockServerSynStreamWithId:2 last:NO]; + [self mockServerHeadersFrameForPushWithId:2 last:NO]; + SPDYMockURLProtocolClient *pushClient2 = [self attachToPushRequestWithUrl:@"http://mocked/pushed"].client; + + // Send another SYN_STREAM from server to client + NSDictionary *headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/pushed4"}; + [self mockServerSynStreamWithId:4 last:NO headers:headers]; + [self mockServerHeadersFrameForPushWithId:4 last:YES]; + SPDYMockURLProtocolClient *pushClient4 = [self attachToPushRequestWithUrl:@"http://mocked/pushed4"].client; + + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)0); + XCTAssertTrue(_mockURLProtocolClient.calledDidReceiveResponse); + XCTAssertFalse(_mockURLProtocolClient.calledDidFinishLoading); + XCTAssertTrue(pushClient2.calledDidReceiveResponse); + XCTAssertFalse(pushClient2.calledDidFinishLoading); + XCTAssertTrue(pushClient4.calledDidReceiveResponse); + XCTAssertTrue(pushClient4.calledDidFinishLoading); + + SPDYMetadata *metadata = [SPDYProtocol metadataForResponse:pushClient4.lastResponse]; + XCTAssertNotNil(metadata); + + // Close original + [self mockServerDataFrameWithId:1 length:1 last:YES]; + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)0); + XCTAssertTrue(_mockURLProtocolClient.calledDidFinishLoading); + XCTAssertFalse(pushClient2.calledDidFinishLoading); + + // Close push 1 + [self mockServerDataFrameWithId:2 length:2 last:YES]; + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)0); + XCTAssertTrue(pushClient2.calledDidFinishLoading); +} + +#if 0 + +- (void)testSYNStreamClosesAfterHeadersMakesCompletionBlockCallback +{ + _mockExtendedDelegate.testSetsPushResponseDataDelegate = NO; + + // Exchange initial SYN_STREAM and SYN_REPLY + [self mockSynStreamAndReply]; + + // Send SYN_STREAM from server to client with 'last' bit set. + [self mockServerSynStreamWithId:2 last:YES]; + [self mockServerHeadersFrameForPushWithId:2 last:NO]; + XCTAssertTrue([self waitForAnyCallbackOrFrame]); // for extended delegate + XCTAssertNotNil(_mockExtendedDelegate.lastPushResponse); + + // Got the completion block callback indicating push response is done? + XCTAssertTrue([self waitForAnyCallbackOrFrame]); + XCTAssertNotNil(_mockPushResponseDataDelegate.lastCompletionPushResponse); + XCTAssertEqual(_mockPushResponseDataDelegate.lastCompletionData.length, (NSUInteger)0); + XCTAssertNil(_mockPushResponseDataDelegate.lastCompletionError); +} + +- (void)testSYNStreamClosesAfterDataWithDelayedExtendedCallbackMakesCompletionBlockCallback +{ + _mockExtendedDelegate.testSetsPushResponseCache = [[NSURLCache alloc] init]; + + // Send server SYN_STREAM, then send all data, before scheduling the run loop and + // allowing the extended delegate callback to happen. Should be all ok. + [self mockSynStreamAndReply]; + [self mockServerSynStreamWithId:2 last:NO]; + [self mockServerHeadersFrameForPushWithId:2 last:NO]; + [self mockServerDataFrameWithId:2 length:1 last:YES]; + XCTAssertTrue([self waitForAnyCallbackOrFrame]); + XCTAssertTrue([self waitForAnyCallbackOrFrame]); + + XCTAssertNotNil(_mockExtendedDelegate.lastPushResponse); + XCTAssertNotNil(_mockPushResponseDataDelegate.lastCompletionPushResponse); + XCTAssertEqual(_mockPushResponseDataDelegate.lastCompletionData.length, (NSUInteger)1); + XCTAssertNil(_mockPushResponseDataDelegate.lastCompletionError); +} + +- (void)testSYNStreamWithDataMakesCompletionBlockCallback +{ + // Disable delegate and cache + _mockExtendedDelegate.testSetsPushResponseDataDelegate = nil; + _mockExtendedDelegate.testSetsPushResponseCache = nil; + + // Initial mainline SYN_STREAM, SYN_REPLY, server SYN_STREAM exchanges + [self mockSynStreamAndReply]; + [self mockServerSynStreamWithId:2 last:NO]; + [self mockServerHeadersFrameForPushWithId:2 last:NO]; + XCTAssertTrue([self waitForAnyCallbackOrFrame]); + XCTAssertNotNil(_mockExtendedDelegate.lastPushResponse); + + // Send DATA frame, verify callback made + [self mockServerDataFrameWithId:2 length:100 last:YES]; + XCTAssertTrue([self waitForAnyCallbackOrFrame]); + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)0); // no errors + XCTAssertNotNil(_mockPushResponseDataDelegate.lastCompletionPushResponse); + XCTAssertNotNil(_mockPushResponseDataDelegate.lastCompletionPushRequest); + XCTAssertNotNil(_mockPushResponseDataDelegate.lastCompletionData); + XCTAssertEqual(_mockPushResponseDataDelegate.lastCompletionData.length, (NSUInteger)100); + XCTAssertNil(_mockPushResponseDataDelegate.lastCompletionError); + + // Some sanity checks + XCTAssertEqualObjects(_mockExtendedDelegate.lastPushResponse, _mockPushResponseDataDelegate.lastCompletionPushResponse); + XCTAssertEqualObjects(_mockExtendedDelegate.lastPushRequest, _mockPushResponseDataDelegate.lastCompletionPushRequest); +} + + - (void)testSYNStreamWithChunkedDataMakesCompletionBlockCallback + { + // Disable delegate + _mockExtendedDelegate.testSetsPushResponseDataDelegate = nil; + [self mockPushResponseWithTwoDataFrames]; + + XCTAssertNotNil(_mockPushResponseDataDelegate.lastCompletionPushResponse); + XCTAssertNotNil(_mockPushResponseDataDelegate.lastCompletionPushRequest); + XCTAssertNotNil(_mockPushResponseDataDelegate.lastCompletionData); + XCTAssertEqual(_mockPushResponseDataDelegate.lastCompletionData.length, (NSUInteger)201); + XCTAssertNil(_mockPushResponseDataDelegate.lastCompletionError); +} + +- (void)testSYNStreamClosedRespondsWithResetAndMakesCompletionBlockCallback +{ + // Initial mainline SYN_STREAM, SYN_REPLY, server SYN_STREAM exchanges + [self mockSynStreamAndReply]; + [self mockServerSynStreamWithId:2 last:NO]; + [self mockServerHeadersFrameForPushWithId:2 last:NO]; + XCTAssertTrue([self waitForAnyCallbackOrFrame]); + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)0); // no errors + + // Cancel it + // @@@ Uh, how to do this? + XCTAssertTrue([self waitForAnyCallbackOrFrame]); + + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)1); + XCTAssertTrue([_mockDecoderDelegate.lastFrame isKindOfClass:[SPDYRstStreamFrame class]]); + XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.lastFrame).statusCode, SPDY_STREAM_CANCEL); + XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.lastFrame).streamId, (SPDYStreamId)2); + + XCTAssertNotNil(_mockPushResponseDataDelegate.lastCompletionPushResponse); + XCTAssertNotNil(_mockPushResponseDataDelegate.lastCompletionPushRequest); + XCTAssertNil(_mockPushResponseDataDelegate.lastCompletionData); + XCTAssertNotNil(_mockPushResponseDataDelegate.lastCompletionError); +} + +- (void)testSYNStreamWithChunkedDataMakesDataDelegateCallbacks +{ + // Disable completion block + _mockExtendedDelegate.testSetsPushResponseCompletionBlock = nil; + + // Initial mainline SYN_STREAM, SYN_REPLY, server SYN_STREAM exchanges + [self mockSynStreamAndReply]; + [self mockServerSynStreamWithId:2 last:NO]; + [self mockServerHeadersFrameForPushWithId:2 last:NO]; + XCTAssertTrue([self waitForAnyCallbackOrFrame]); + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)0); // no errors + XCTAssertNotNil(_mockExtendedDelegate.lastPushResponse); + + // Send DATA frame, verify callback made + [self mockServerDataFrameWithId:2 length:100 last:NO]; + XCTAssertTrue([self waitForAnyCallbackOrFrame]); + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)0); // no errors + XCTAssertNotNil(_mockPushResponseDataDelegate.lastRequest); + XCTAssertNotNil(_mockPushResponseDataDelegate.lastData); + XCTAssertEqual(_mockPushResponseDataDelegate.lastData.length, (NSUInteger)100); + + // Send last DATA frame + [self mockServerDataFrameWithId:2 length:101 last:YES]; + XCTAssertTrue([self waitForAnyCallbackOrFrame]); + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)0); // no errors + XCTAssertNotNil(_mockPushResponseDataDelegate.lastRequest); + XCTAssertNotNil(_mockPushResponseDataDelegate.lastData); + XCTAssertEqual(_mockPushResponseDataDelegate.lastData.length, (NSUInteger)101); + + // Runloop may have scheduled the final didComplete callback before we could stop it. But + // if not, wait for it. + if (_mockPushResponseDataDelegate.lastMetadata == nil) { + XCTAssertTrue([self waitForAnyCallbackOrFrame]); + } + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)0); // no errors + XCTAssertNotNil(_mockPushResponseDataDelegate.lastMetadata); + XCTAssertEqualObjects(_mockPushResponseDataDelegate.lastMetadata[SPDYMetadataVersionKey], @"3.1"); + XCTAssertEqualObjects(_mockPushResponseDataDelegate.lastMetadata[SPDYMetadataStreamIdKey], @"2"); + XCTAssertTrue([_mockPushResponseDataDelegate.lastMetadata[SPDYMetadataStreamRxBytesKey] integerValue] > 0); + XCTAssertTrue([_mockPushResponseDataDelegate.lastMetadata[SPDYMetadataStreamTxBytesKey] integerValue] == 0); + + // Some sanity checks + XCTAssertEqualObjects(_mockExtendedDelegate.lastPushRequest, _mockPushResponseDataDelegate.lastRequest); + XCTAssertNil(_mockPushResponseDataDelegate.lastError); +} + +- (void)testSYNStreamWithChunkedDataMakesDataDelegateAndCompletionBlockCallbacks +{ + // Disable caching + _mockExtendedDelegate.testSetsPushResponseCache = nil; + + // Enable both completion block and delegate (default) + [self mockPushResponseWithTwoDataFrames]; + + // Verify last chunk received + XCTAssertEqual(_mockPushResponseDataDelegate.lastData.length, (NSUInteger)101); + + // Ensure both happened + XCTAssertNotNil(_mockPushResponseDataDelegate.lastCompletionPushResponse); + XCTAssertNil(_mockPushResponseDataDelegate.lastCompletionError); + XCTAssertEqual(_mockPushResponseDataDelegate.lastCompletionData.length, (NSUInteger)201); + XCTAssertNil(_mockPushResponseDataDelegate.lastError); + XCTAssertNotNil(_mockPushResponseDataDelegate.lastMetadata); +} + +- (void)testSYNStreamWithChunkedDataAndCustomCacheCachesResponse +{ + // Enabled caching only + _mockExtendedDelegate.testSetsPushResponseDataDelegate = nil; + _mockExtendedDelegate.testSetsPushResponseCompletionBlock = nil; + _mockExtendedDelegate.testSetsPushResponseCache = [[NSURLCache alloc] init]; + + // Make a new request, don't use the NSURLRequest that got pushed to us + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://mocked/pushed"]]; + + // Sanity check + NSCachedURLResponse *response = [_mockExtendedDelegate.testSetsPushResponseCache cachedResponseForRequest:request]; + XCTAssertNil(response); + + [self mockPushResponseWithTwoDataFrames]; + + // Ensure neither callback happened + XCTAssertNil(_mockPushResponseDataDelegate.lastCompletionPushResponse); + XCTAssertNil(_mockPushResponseDataDelegate.lastMetadata); + + response = [_mockExtendedDelegate.testSetsPushResponseCache cachedResponseForRequest:request]; + XCTAssertNotNil(response); + XCTAssertEqual(response.data.length, (NSUInteger)201); + XCTAssertEqualObjects(((NSHTTPURLResponse *)response.response).allHeaderFields[@"PushHeader"], @"PushValue"); +} + +- (void)testSYNStreamWithChunkedDataAndDelegateSetsNilCacheDoesNotCacheResponse +{ + // Enable nothing, but we still make the completion callback in didReceiveResponse + _mockExtendedDelegate.testSetsPushResponseDataDelegate = nil; + _mockExtendedDelegate.testSetsPushResponseCompletionBlock = nil; + _mockExtendedDelegate.testSetsPushResponseCache = nil; + + // Sanity check + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://mocked/pushed"]]; + NSCachedURLResponse *response = [[NSURLCache sharedURLCache] cachedResponseForRequest:request]; + XCTAssertNil(response); + + [self mockPushResponseWithTwoDataFrames]; + + response = [[NSURLCache sharedURLCache] cachedResponseForRequest:request]; + XCTAssertNil(response); +} + +- (void)testSYNStreamWithChunkedDataAndDefaultCacheAndNoDelegateCachesResponse +{ + // Disable extended delegate + [_URLRequest setExtendedDelegate:nil inRunLoop:nil forMode:nil]; + _protocolRequest = [[SPDYProtocol alloc] initWithRequest:_URLRequest cachedResponse:nil client:nil]; + + // Sanity check + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://mocked/pushed"]]; + NSCachedURLResponse *response = [[NSURLCache sharedURLCache] cachedResponseForRequest:request]; + XCTAssertNil(response); + + // No callbacks to wait for + [self mockSynStreamAndReply]; + [self mockServerSynStreamWithId:2 last:NO]; + [self mockServerHeadersFrameForPushWithId:2 last:NO]; + [self mockServerDataFrameWithId:2 length:100 last:NO]; + [self mockServerDataFrameWithId:2 length:101 last:YES]; + + response = [[NSURLCache sharedURLCache] cachedResponseForRequest:request]; + XCTAssertNotNil(response); + XCTAssertEqual(response.data.length, (NSUInteger)201); + XCTAssertEqualObjects(((NSHTTPURLResponse *)response.response).allHeaderFields[@"PushHeader"], @"PushValue"); +} + +#endif + +#pragma mark Headers-related push tests + +- (void)testSYNStreamAndAHeadersFrameMergesValues +{ + [self mockSynStreamAndReplyWithId:1 last:NO]; + [self mockServerSynStreamWithId:2 last:NO]; + [self mockServerHeadersFrameForPushWithId:2 last:NO]; + SPDYMockURLProtocolClient *pushClient = [self attachToPushRequestWithUrl:@"http://mocked/pushed"].client; + + XCTAssertTrue(pushClient.calledDidReceiveResponse); + XCTAssertEqualObjects([pushClient.lastResponse.allHeaderFields valueForKey:@"PushHeader"], @"PushValue"); + XCTAssertEqualObjects([pushClient.lastResponse.allHeaderFields valueForKey:@"PushHeader2"], nil); + + // Send HEADERS frame + NSDictionary *headers = @{@"PushHeader2":@"PushValue2"}; + [self mockServerHeadersFrameWithId:2 headers:headers last:NO]; + + // TODO: no way to expose new headers to URLProtocolClient, can't verify presence of new header + // except to say nothing crashed here. +} + +- (void)testSYNStreamAndAHeadersFrameAfterDataIgnoresValues +{ + [self mockSynStreamAndReplyWithId:1 last:NO]; + [self mockServerSynStreamWithId:2 last:NO]; + [self mockServerHeadersFrameForPushWithId:2 last:NO]; + SPDYMockURLProtocolClient *pushClient = [self attachToPushRequestWithUrl:@"http://mocked/pushed"].client; + XCTAssertTrue(pushClient.calledDidReceiveResponse); + + // Send DATA frame + [self mockServerDataFrameWithId:2 length:100 last:NO]; + XCTAssertTrue(pushClient.calledDidLoadData); + + // Send last HEADERS frame + NSDictionary *headers = @{@"PushHeader2":@"PushValue2"}; + [self mockServerHeadersFrameWithId:2 headers:headers last:YES]; + + // Ensure stream was closed and callback made + XCTAssertTrue(pushClient.calledDidFinishLoading); + + // TODO: no way to expose new headers to URLProtocolClient, can't verify absence of new header. +} + +#if 0 + +#pragma mark Cache-related tests + +- (void)testSYNStreamWithChunkedDataDoesNotCacheWhenSuggestedResponseIsNil +{ + _mockExtendedDelegate.testSetsPushResponseCache = [[NSURLCache alloc] init]; + _mockPushResponseDataDelegate.willCacheShouldReturnNil = YES; + + // Make a new request, don't use the NSURLRequest that got pushed to us + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://mocked/pushed"]]; + + [self mockPushResponseWithTwoDataFrames]; + + XCTAssertNotNil(_mockPushResponseDataDelegate.lastWillCacheSuggestedResponse); + + NSCachedURLResponse *response = [_mockExtendedDelegate.testSetsPushResponseCache cachedResponseForRequest:request]; + XCTAssertNil(response); +} + +- (void)testSYNStreamWithChunkedDataDoesCacheSuggestedResponse +{ + _mockExtendedDelegate.testSetsPushResponseCache = [[NSURLCache alloc] init]; + + // Make a new request, don't use the NSURLRequest that got pushed to us + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://mocked/pushed"]]; + + [self mockPushResponseWithTwoDataFrames]; + + XCTAssertNotNil(_mockPushResponseDataDelegate.lastWillCacheSuggestedResponse); + XCTAssertEqualObjects(_mockExtendedDelegate.lastPushResponse, _mockPushResponseDataDelegate.lastWillCacheSuggestedResponse.response); + + NSCachedURLResponse *response = [_mockExtendedDelegate.testSetsPushResponseCache cachedResponseForRequest:request]; + XCTAssertNotNil(response); + XCTAssertEqual(response.data.length, (NSUInteger)201); + XCTAssertEqualObjects(((NSHTTPURLResponse *)response.response).allHeaderFields[@"PushHeader"], @"PushValue"); +} + +- (void)testSYNStreamWithChunkedDataDoesCacheCustomSuggestedResponse +{ + //_mockExtendedDelegate.testSetsPushResponseCache = [[NSURLCache alloc] init]; + NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 + diskCapacity:20 * 1024 * 1024 + diskPath:nil]; + _mockExtendedDelegate.testSetsPushResponseCache = URLCache; + + [self mockPushResponseWithTwoDataFramesWithId:2]; + XCTAssertNotNil(_mockPushResponseDataDelegate.lastWillCacheSuggestedResponse); + XCTAssertEqualObjects(_mockExtendedDelegate.lastPushResponse, _mockPushResponseDataDelegate.lastWillCacheSuggestedResponse.response); + NSCachedURLResponse *lastCachedResponse = _mockPushResponseDataDelegate.lastWillCacheSuggestedResponse; + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://mocked/pushed"]]; + NSCachedURLResponse *response = [_mockExtendedDelegate.testSetsPushResponseCache cachedResponseForRequest:request]; + XCTAssertEqual(response.data.length, (NSUInteger)201); + + NSCachedURLResponse *newCachedResponse = [[NSCachedURLResponse alloc] + initWithResponse:lastCachedResponse.response + data:[NSMutableData dataWithLength:1] // mutated + userInfo:nil + storagePolicy:_mockPushResponseDataDelegate.lastWillCacheSuggestedResponse.storagePolicy]; + + _mockPushResponseDataDelegate.willCacheReturnOverride = newCachedResponse; + + // Do it a again. First one was just to grab a response. + [self mockPushResponseWithTwoDataFramesWithId:4]; + + response = [_mockExtendedDelegate.testSetsPushResponseCache cachedResponseForRequest:request]; + XCTAssertEqual(response.data.length, (NSUInteger)1); + XCTAssertEqualObjects(((NSHTTPURLResponse *)response.response).allHeaderFields[@"PushHeader"], @"PushValue"); +} + +- (void)testSYNStreamWithChunkedDataDoesNotCache500Response +{ + _mockExtendedDelegate.testSetsPushResponseCache = [[NSURLCache alloc] init]; + + NSDictionary *headers = @{@":status":@"500", @":version":@"http/1.1", @"PushHeader":@"PushValue"}; + [self mockSynStreamAndReply]; + [self mockServerSynStreamWithId:2 last:NO]; + [self mockServerHeadersFrameWithId:2 headers:headers last:NO]; + XCTAssertTrue([self waitForAnyCallbackOrFrame]); + [self mockServerDataFrameWithId:2 length:1 last:YES]; + XCTAssertTrue([self waitForAnyCallbackOrFrame]); + + XCTAssertNotNil(_mockExtendedDelegate.lastPushResponse); + XCTAssertEqual(_mockExtendedDelegate.lastPushResponse.statusCode, (NSInteger)500); + XCTAssertNil(_mockPushResponseDataDelegate.lastWillCacheSuggestedResponse); + XCTAssertNotNil(_mockPushResponseDataDelegate.lastMetadata); + XCTAssertNil(_mockPushResponseDataDelegate.lastError); + + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://mocked/pushed"]]; + NSCachedURLResponse *response = [_mockExtendedDelegate.testSetsPushResponseCache cachedResponseForRequest:request]; + XCTAssertNil(response); +} +#endif + @end diff --git a/SPDYUnitTests/SPDYSessionManagerTest.m b/SPDYUnitTests/SPDYSessionManagerTest.m index 2629b03..11bf092 100644 --- a/SPDYUnitTests/SPDYSessionManagerTest.m +++ b/SPDYUnitTests/SPDYSessionManagerTest.m @@ -96,7 +96,7 @@ - (void)testDispatchQueuedStreamThenDoubleCanceledDoesReleaseStreamAndNotAssert SPDYProtocol *protocol = [[SPDYProtocol alloc] init]; SPDYStream * __weak weakStream = nil; @autoreleasepool { - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol]; + SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol pushStreamManager:nil]; weakStream = stream; urlRequest.SPDYDeferrableInterval = 0; stream.request = urlRequest; @@ -153,7 +153,7 @@ - (void)testAllSessionsClosingDoesFailPendingStreams [SPDYProtocol setConfiguration:configuration]; SPDYProtocol *protocol = [[SPDYProtocol alloc] init]; - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol]; + SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol pushStreamManager:nil]; urlRequest.SPDYDeferrableInterval = 0; stream.request = urlRequest; @@ -195,7 +195,7 @@ - (void)testDispatchQueuedStreamThenSessionClosesDoesReleaseStreamAndRemoveSessi SPDYProtocol *protocol = [[SPDYProtocol alloc] init]; SPDYStream * __weak weakStream = nil; @autoreleasepool { - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol]; + SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol pushStreamManager:nil]; weakStream = stream; urlRequest.SPDYDeferrableInterval = 0; stream.request = urlRequest; @@ -236,9 +236,9 @@ - (void)testReachabilityChangesAfterQueueingStreamDoesDispatchNewStreamToNewSess SPDYProtocol *protocol = [[SPDYProtocol alloc] init]; SPDYProtocol *protocol2 = [[SPDYProtocol alloc] init]; - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol]; + SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol pushStreamManager:nil]; stream.request = urlRequest; - SPDYStream *stream2 = [[SPDYStream alloc] initWithProtocol:protocol2]; + SPDYStream *stream2 = [[SPDYStream alloc] initWithProtocol:protocol2 pushStreamManager:nil]; stream2.request = urlRequest; // Force reachability and queue stream1 @@ -295,10 +295,10 @@ - (void)testQueueStreamToWrongPoolAndReachabilityChangesBeforeConnectionFailsDoe SPDYSocket *socket = [[SPDYSocket alloc] initWithDelegate:nil]; SPDYProtocol *protocol = [[SPDYProtocol alloc] init]; - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol]; + SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol pushStreamManager:nil]; stream.request = urlRequest; SPDYProtocol *protocol2 = [[SPDYProtocol alloc] init]; - SPDYStream *stream2 = [[SPDYStream alloc] initWithProtocol:protocol2]; + SPDYStream *stream2 = [[SPDYStream alloc] initWithProtocol:protocol2 pushStreamManager:nil]; stream2.request = urlRequest; // Force reachability and queue stream1 @@ -366,7 +366,7 @@ - (void)_commonSocketAndGlobalReachabilityChangesAfterQueueingStreamDoesUpdateSe SPDYSocket *socket = [[SPDYSocket alloc] initWithDelegate:nil]; SPDYProtocol *protocol = [[SPDYProtocol alloc] init]; - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol]; + SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol pushStreamManager:nil]; stream.request = urlRequest; // Force reachability to WIFI and queue stream @@ -429,7 +429,7 @@ - (void)_commonSocketReachabilityChangesAfterQueueingStreamThenGlobalReachabilit SPDYSocket *socket = [[SPDYSocket alloc] initWithDelegate:nil]; SPDYProtocol *protocol = [[SPDYProtocol alloc] init]; - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol]; + SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:protocol pushStreamManager:nil]; stream.request = urlRequest; // Force reachability to WIFI and queue stream diff --git a/SPDYUnitTests/SPDYSessionTest.m b/SPDYUnitTests/SPDYSessionTest.m index 3faae79..6e959f4 100644 --- a/SPDYUnitTests/SPDYSessionTest.m +++ b/SPDYUnitTests/SPDYSessionTest.m @@ -95,7 +95,7 @@ - (void)testReceivedMetadataForSingleShortRequestWithBody - (void)testReceivedStreamTimingsMetadataForSingleShortRequest { - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol]]; + SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol] pushStreamManager:nil]; [_session openStream:stream]; XCTAssertTrue([_mockDecoderDelegate.lastFrame isKindOfClass:[SPDYSynStreamFrame class]]); [_mockDecoderDelegate clear]; @@ -163,11 +163,11 @@ - (void)testReceiveGOAWAYWithInFlightStreamDoesResetStreams [self mockSynStreamAndReplyWithId:1 last:YES]; // Send two SYN_STREAMs only, no reply - [_session openStream:[[SPDYStream alloc] initWithProtocol:[self createProtocol]]]; + [_session openStream:[[SPDYStream alloc] initWithProtocol:[self createProtocol] pushStreamManager:nil]]; XCTAssertTrue([_mockDecoderDelegate.lastFrame isKindOfClass:[SPDYSynStreamFrame class]]); [_mockDecoderDelegate clear]; - [_session openStream:[[SPDYStream alloc] initWithProtocol:[self createProtocol]]]; + [_session openStream:[[SPDYStream alloc] initWithProtocol:[self createProtocol] pushStreamManager:nil]]; XCTAssertTrue([_mockDecoderDelegate.lastFrame isKindOfClass:[SPDYSynStreamFrame class]]); [_mockDecoderDelegate clear]; @@ -203,7 +203,7 @@ - (void)testSendDATABufferRemainsValidAfterRequestIsDone // 1.) Issue a HTTP request towards the server, this will send the SYN_STREAM request and wait // for the SYN_REPLY. It will use stream-id of 1 since it's the first request. - [_session openStream:[[SPDYStream alloc] initWithProtocol:protocolRequest]]; + [_session openStream:[[SPDYStream alloc] initWithProtocol:protocolRequest pushStreamManager:nil]]; XCTAssertTrue([_mockDecoderDelegate.framesReceived[0] isKindOfClass:[SPDYSynStreamFrame class]]); XCTAssertTrue([_mockDecoderDelegate.framesReceived[1] isKindOfClass:[SPDYDataFrame class]]); XCTAssertTrue(((SPDYDataFrame *)_mockDecoderDelegate.framesReceived[1]).last); @@ -289,7 +289,7 @@ - (void)testReceiveDATABeforeSYNREPLYDoesResetAndCloseStream NSMutableData *data = [NSMutableData dataWithLength:1]; // Send a SYN_STREAM, no reply - [_session openStream:[[SPDYStream alloc] initWithProtocol:[self createProtocol]]]; + [_session openStream:[[SPDYStream alloc] initWithProtocol:[self createProtocol] pushStreamManager:nil]]; XCTAssertTrue([_mockDecoderDelegate.lastFrame isKindOfClass:[SPDYSynStreamFrame class]]); [_mockDecoderDelegate clear]; @@ -366,7 +366,7 @@ - (void)testMergeHeadersWithLocationAnd200DoesRedirect _URLRequest.SPDYDeferrableInterval = 1.0; [_URLRequest setValue:@"50" forHTTPHeaderField:@"content-length"]; - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol]]; + SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol] pushStreamManager:nil]; [stream startWithStreamId:1 sendWindowSize:1024 receiveWindowSize:1024]; NSDictionary *headers = @{@":scheme":@"https", @":host":@"mocked", @":path":@"/init", @@ -398,7 +398,7 @@ - (void)testMergeHeadersWithLocationAnd302DoesRedirectToGET _URLRequest.SPDYBodyFile = @"bodyfile.txt"; [_URLRequest setValue:@"50" forHTTPHeaderField:@"content-length"]; - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol]]; + SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol] pushStreamManager:nil]; [stream startWithStreamId:1 sendWindowSize:1024 receiveWindowSize:1024]; NSDictionary *headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/init", @@ -426,7 +426,7 @@ - (void)testMergeHeadersWithLocationAnd303DoesRedirectToGET _URLRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://mocked/init"]]; _URLRequest.HTTPMethod = @"POST"; _URLRequest.HTTPBodyStream = inputStream; // test stream this time - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol]]; + SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol] pushStreamManager:nil]; [stream startWithStreamId:1 sendWindowSize:1024 receiveWindowSize:1024]; NSDictionary *headers = @{@":scheme":@"https", @":host":@"mocked", @":path":@"/init", diff --git a/SPDYUnitTests/SPDYStreamTest.m b/SPDYUnitTests/SPDYStreamTest.m index ebb5efe..3f7c19f 100644 --- a/SPDYUnitTests/SPDYStreamTest.m +++ b/SPDYUnitTests/SPDYStreamTest.m @@ -90,7 +90,7 @@ - (void)testStreamingWithStream - (void)testMergeHeadersCollisionDoesAbort { - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol]]; + SPDYStream *stream = [self createStream]; [stream startWithStreamId:1 sendWindowSize:1024 receiveWindowSize:1024]; NSDictionary *headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/init", @@ -106,7 +106,7 @@ - (void)testMergeHeadersCollisionDoesAbort - (void)testReceiveResponseMissingStatusCodeDoesAbort { - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol]]; + SPDYStream *stream = [self createStream]; [stream startWithStreamId:1 sendWindowSize:1024 receiveWindowSize:1024]; NSDictionary *headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/init", @@ -119,7 +119,7 @@ - (void)testReceiveResponseMissingStatusCodeDoesAbort - (void)testReceiveResponseInvalidStatusCodeDoesAbort { - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol]]; + SPDYStream *stream = [self createStream]; [stream startWithStreamId:1 sendWindowSize:1024 receiveWindowSize:1024]; NSDictionary *headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/init", @@ -132,7 +132,7 @@ - (void)testReceiveResponseInvalidStatusCodeDoesAbort - (void)testReceiveResponseMissingVersionDoesAbort { - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol]]; + SPDYStream *stream = [self createStream]; [stream startWithStreamId:1 sendWindowSize:1024 receiveWindowSize:1024]; NSDictionary *headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/init", @@ -145,7 +145,7 @@ - (void)testReceiveResponseMissingVersionDoesAbort - (void)testReceiveResponseDoesSucceed { - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol]]; + SPDYStream *stream = [self createStream]; [stream startWithStreamId:1 sendWindowSize:1024 receiveWindowSize:1024]; NSDictionary *headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/init", @@ -173,7 +173,7 @@ - (void)testReceiveResponseWithLocationDoesRedirect _URLRequest.SPDYBodyFile = @"bodyfile.txt"; _URLRequest.SPDYDeferrableInterval = 1.0; - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol]]; + SPDYStream *stream = [self createStream]; [stream startWithStreamId:1 sendWindowSize:1024 receiveWindowSize:1024]; NSDictionary *headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/init", @@ -202,7 +202,7 @@ - (void)testReceiveResponseWithLocationAnd303DoesRedirect _URLRequest.HTTPMethod = @"POST"; _URLRequest.SPDYBodyStream = inputStream; - SPDYStream *stream = [[SPDYStream alloc] initWithProtocol:[self createProtocol]]; + SPDYStream *stream = [self createStream]; [stream startWithStreamId:1 sendWindowSize:1024 receiveWindowSize:1024]; NSDictionary *headers = @{@":scheme":@"http", @":host":@"mocked", @":path":@"/init", From 2e76c0363de30dc70628c5f1df93f7be72551177 Mon Sep 17 00:00:00 2001 From: Kevin Goodier Date: Tue, 7 Jul 2015 18:44:29 -0700 Subject: [PATCH 3/9] Add NSNotification for new push requests. Post an NSNotification when a push request is received. This will allow the app to use the provided NSURLRequest and filter for ones of interest, potentially starting a new load for that request. CocoaSPDY will hook up that request to start loading to the pushed request using SPDYPushStreamManager. --- SPDY/SPDYProtocol.h | 11 ++++++++ SPDY/SPDYProtocol.m | 1 + SPDY/SPDYStream.m | 7 ++++- SPDYUnitTests/SPDYMockSessionTestBase.h | 1 + SPDYUnitTests/SPDYMockSessionTestBase.m | 9 +++++-- SPDYUnitTests/SPDYServerPushTest.m | 35 +++++++++++++++++++++++++ 6 files changed, 61 insertions(+), 3 deletions(-) diff --git a/SPDY/SPDYProtocol.h b/SPDY/SPDYProtocol.h index a98108c..e9e34cc 100644 --- a/SPDY/SPDYProtocol.h +++ b/SPDY/SPDYProtocol.h @@ -12,9 +12,20 @@ #import #import "SPDYLogger.h" + +/** + NSNotification calls are posted using the NSURL loader thread. Care must be taken to + not block or delay the execution of this thread. Any substantive work should be done + on a different thread. +*/ + +// userInfo has a key "origin", value is an NSString extern NSString *const SPDYOriginRegisteredNotification; extern NSString *const SPDYOriginUnregisteredNotification; +// userInfo has a key "request", value is an NSURLRequest +extern NSString *const SPDYPushRequestReceivedNotification; + @class SPDYConfiguration; @protocol SPDYTLSTrustEvaluator; diff --git a/SPDY/SPDYProtocol.m b/SPDY/SPDYProtocol.m index 90fb4d0..a3147df 100644 --- a/SPDY/SPDYProtocol.m +++ b/SPDY/SPDYProtocol.m @@ -33,6 +33,7 @@ NSString *const SPDYSocketErrorDomain = @"SPDYSocketErrorDomain"; NSString *const SPDYOriginRegisteredNotification = @"SPDYOriginRegisteredNotification"; NSString *const SPDYOriginUnregisteredNotification = @"SPDYOriginUnregisteredNotification"; +NSString *const SPDYPushRequestReceivedNotification = @"SPDYPushRequestReceivedNotification"; static char *const SPDYConfigQueue = "com.twitter.SPDYConfigQueue"; diff --git a/SPDY/SPDYStream.m b/SPDY/SPDYStream.m index 4f1c6b0..e3aeefd 100644 --- a/SPDY/SPDYStream.m +++ b/SPDY/SPDYStream.m @@ -622,12 +622,17 @@ - (void)didReceivePushRequest timeoutInterval:_request.timeoutInterval]; requestCopy.allHTTPHeaderFields = _request.allHTTPHeaderFields; requestCopy.HTTPMethod = @"GET"; - requestCopy.SPDYPriority = (NSUInteger)_priority; + requestCopy.SPDYPriority = (NSUInteger)_priority; // TODO: same or +1 (lower priority)? _pushRequest = [SPDYProtocol canonicalRequestForRequest:requestCopy]; _request = _pushRequest; // need a strong reference for _request's weak one [_pushStreamManager addStream:self associatedWith:_associatedStream]; + + // Fire global notification on current thread + [[NSNotificationCenter defaultCenter] postNotificationName:SPDYPushRequestReceivedNotification + object:nil + userInfo:@{ @"request": _request }]; } - (void)didLoadData:(NSData *)data diff --git a/SPDYUnitTests/SPDYMockSessionTestBase.h b/SPDYUnitTests/SPDYMockSessionTestBase.h index 9da24af..fffee48 100644 --- a/SPDYUnitTests/SPDYMockSessionTestBase.h +++ b/SPDYUnitTests/SPDYMockSessionTestBase.h @@ -66,6 +66,7 @@ typedef void (^SPDYAsyncTestCallback)(); - (SPDYStream *)createStream; - (void)makeSessionReadData:(NSData *)data; - (SPDYStream *)attachToPushRequestWithUrl:(NSString *)url; +- (SPDYStream *)attachToPushRequest:(NSURLRequest *)request; - (SPDYStream *)mockSynStreamAndReplyWithId:(SPDYStreamId)streamId last:(bool)last; - (void)mockServerSynReplyWithId:(SPDYStreamId)streamId last:(BOOL)last; diff --git a/SPDYUnitTests/SPDYMockSessionTestBase.m b/SPDYUnitTests/SPDYMockSessionTestBase.m index 5d4451e..fcad40d 100644 --- a/SPDYUnitTests/SPDYMockSessionTestBase.m +++ b/SPDYUnitTests/SPDYMockSessionTestBase.m @@ -128,10 +128,15 @@ - (void)makeSocketConnect } - (SPDYStream *)attachToPushRequestWithUrl:(NSString *)url +{ + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; + return [self attachToPushRequest:request]; +} + +- (SPDYStream *)attachToPushRequest:(NSURLRequest *)request { _mockPushURLProtocolClient = [[SPDYMockURLProtocolClient alloc] init]; - NSMutableURLRequest *pushURLRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; - SPDYProtocol *pushProtocolRequest = [[SPDYProtocol alloc] initWithRequest:pushURLRequest cachedResponse:nil client:_mockPushURLProtocolClient]; + SPDYProtocol *pushProtocolRequest = [[SPDYProtocol alloc] initWithRequest:request cachedResponse:nil client:_mockPushURLProtocolClient]; [_pushProtocolList addObject:pushProtocolRequest]; SPDYStream *pushStream = [_pushStreamManager streamForProtocol:pushProtocolRequest]; diff --git a/SPDYUnitTests/SPDYServerPushTest.m b/SPDYUnitTests/SPDYServerPushTest.m index e79d0f3..9a6b831 100644 --- a/SPDYUnitTests/SPDYServerPushTest.m +++ b/SPDYUnitTests/SPDYServerPushTest.m @@ -178,6 +178,41 @@ - (void)testSYNStreamWithStreamIDNonZeroMakesResponseCallback XCTAssertEqualObjects([pushResponse.allHeaderFields valueForKey:@"PushHeader"], @"PushValue"); } +- (void)testSYNStreamWithStreamIDNonZeroPostsNotification +{ + SPDYMockURLProtocolClient __block *pushClient = nil; + + [[NSNotificationCenter defaultCenter] addObserverForName:SPDYPushRequestReceivedNotification object:nil queue:nil usingBlock:^(NSNotification *note) { + XCTAssertTrue([note.userInfo[@"request"] isKindOfClass:[NSURLRequest class]]); + + NSURLRequest *request = note.userInfo[@"request"]; + XCTAssertNotNil(request); + XCTAssertEqualObjects(request.URL.absoluteString, @"http://mocked/pushed"); + + pushClient = [self attachToPushRequest:request].client; + }]; + + // Exchange initial SYN_STREAM and SYN_REPLY + [self mockSynStreamAndReplyWithId:1 last:NO]; + + // Send SYN_STREAM from server to client. Notification posted at this point. + [self mockServerSynStreamWithId:2 last:NO]; + XCTAssertNotNil(pushClient); + XCTAssertFalse(pushClient.calledDidReceiveResponse); + + // Send HEADERS from server to client + [self mockServerHeadersFrameForPushWithId:2 last:NO]; + XCTAssertTrue(pushClient.calledDidReceiveResponse); + XCTAssertFalse(pushClient.calledDidLoadData); + XCTAssertFalse(pushClient.calledDidFailWithError); + XCTAssertFalse(pushClient.calledDidFinishLoading); + + NSHTTPURLResponse *pushResponse = pushClient.lastResponse; + XCTAssertEqualObjects(pushResponse.URL.absoluteString, @"http://mocked/pushed"); + XCTAssertEqual(pushResponse.statusCode, 200); + XCTAssertEqualObjects([pushResponse.allHeaderFields valueForKey:@"PushHeader"], @"PushValue"); +} + - (void)testSYNStreamAfterAssociatedStreamClosesRespondsWithGoAway { // Exchange initial SYN_STREAM and SYN_REPLY From 4b57d2400adefc9cf1adb3588fc0e6a5085fdb9c Mon Sep 17 00:00:00 2001 From: Kevin Goodier Date: Thu, 8 Oct 2015 22:27:32 -0700 Subject: [PATCH 4/9] Enforce same-origin policy for pushed requests. --- SPDY/SPDYSession.m | 4 ---- SPDY/SPDYStream.m | 25 ++++++++++++++++++++++++- SPDYUnitTests/SPDYServerPushTest.m | 16 ++++++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/SPDY/SPDYSession.m b/SPDY/SPDYSession.m index 09bd982..b3c42f2 100644 --- a/SPDY/SPDYSession.m +++ b/SPDY/SPDYSession.m @@ -595,10 +595,6 @@ - (void)didReadSynStreamFrame:(SPDYSynStreamFrame *)synStreamFrame frameDecoder: return; } - // TODO: Browsers receiving a pushed response MUST validate that the server is authorized to - // push the URL using the browser same-origin policy. For example, a SPDY connection to - // www.foo.com is generally not permitted to push a response for www.evil.com. - SPDYStream *stream = [[SPDYStream alloc] initWithAssociatedStream:associatedStream priority:synStreamFrame.priority]; diff --git a/SPDY/SPDYStream.m b/SPDY/SPDYStream.m index e3aeefd..8bd93bd 100644 --- a/SPDY/SPDYStream.m +++ b/SPDY/SPDYStream.m @@ -21,6 +21,7 @@ #import "SPDYCommonLogger.h" #import "SPDYDefinitions.h" #import "SPDYMetadata+Utils.h" +#import "SPDYOrigin.h" #import "SPDYProtocol+Project.h" #import "SPDYPushStreamManager.h" #import "SPDYStopwatch.h" @@ -609,6 +610,29 @@ - (void)didReceivePushRequest return; } + // Browsers receiving a pushed response MUST validate that the server is authorized to + // push the URL using the browser same-origin policy. For example, a SPDY connection to + // www.foo.com is generally not permitted to push a response for www.evil.com. + // Enforce by canonicalizing origins and comparing them. + NSError *error; + NSURL *pushURL = [[NSURL alloc] initWithScheme:scheme host:host path:path]; + SPDYOrigin *pushOrigin = [[SPDYOrigin alloc] initWithURL:pushURL error:&error]; + if (!pushOrigin) { + SPDY_WARNING(@"pushed stream invalid origin: %@", error); + [self abortWithError:error status:SPDY_STREAM_INVALID_STREAM]; + return; + } + + NSURL *associatedURL = _associatedStream.request.URL; + SPDYOrigin *associatedOrigin = [[SPDYOrigin alloc] initWithURL:associatedURL error:&error]; + NSAssert(associatedOrigin, @"original request must have had valid origin"); + + if (![associatedOrigin isEqual:pushOrigin]) { + SPDY_WARNING(@"Pushed URL is not same origin (%@) as associated stream (%@)", pushOrigin, associatedOrigin); + [self abortWithError:error status:SPDY_STREAM_REFUSED_STREAM]; + return; + } + // Because pushed responses have no request, they have no request headers associated with // them. At the framing layer, SPDY pushed streams contain an "associated-stream-id" which // indicates the requested stream for which the pushed stream is related. The pushed @@ -616,7 +640,6 @@ - (void)didReceivePushRequest // of ":host", ":scheme", and ":path", which are provided as part of the pushed response // stream headers. The browser MUST store these inherited and implied request headers // with the cached resource. - NSURL *pushURL = [[NSURL alloc] initWithScheme:scheme host:host path:path]; NSMutableURLRequest *requestCopy = [NSMutableURLRequest requestWithURL:pushURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:_request.timeoutInterval]; diff --git a/SPDYUnitTests/SPDYServerPushTest.m b/SPDYUnitTests/SPDYServerPushTest.m index 9a6b831..6ca976c 100644 --- a/SPDYUnitTests/SPDYServerPushTest.m +++ b/SPDYUnitTests/SPDYServerPushTest.m @@ -155,6 +155,22 @@ - (void)testSYNStreamAndAHeadersFrameWithDuplicatesRespondsWithReset XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.lastFrame).streamId, (SPDYStreamId)2); } +- (void)testSYNStreamWithDifferentOriginRespondsWithReset +{ + // Exchange initial SYN_STREAM and SYN_REPLY + [self mockSynStreamAndReplyWithId:1 last:NO]; + + // Default test origin is "http://mocked:80". Use different scheme for test. + NSDictionary *headers = @{@":scheme":@"https", @":host":@"mocked", @":path":@"/pushed"}; + [self mockServerSynStreamWithId:2 last:NO headers:headers]; + + // Different origin for push, client must refuse + XCTAssertEqual(_mockDecoderDelegate.frameCount, (NSUInteger)1); + XCTAssertTrue([_mockDecoderDelegate.lastFrame isKindOfClass:[SPDYRstStreamFrame class]]); + XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.lastFrame).statusCode, SPDY_STREAM_REFUSED_STREAM); + XCTAssertEqual(((SPDYRstStreamFrame *)_mockDecoderDelegate.lastFrame).streamId, (SPDYStreamId)2); +} + #pragma mark Simple push callback tests - (void)testSYNStreamWithStreamIDNonZeroMakesResponseCallback From 7eb5483aa7252b813bc5f89902895b2a057ca69b Mon Sep 17 00:00:00 2001 From: Kevin Goodier Date: Thu, 8 Oct 2015 22:41:24 -0700 Subject: [PATCH 5/9] Clean up access to headers in SPDYStream. --- SPDY/SPDYStream.h | 1 - SPDY/SPDYStream.m | 24 ++++++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/SPDY/SPDYStream.h b/SPDY/SPDYStream.h index 28004a5..4232944 100644 --- a/SPDY/SPDYStream.h +++ b/SPDY/SPDYStream.h @@ -31,7 +31,6 @@ @property (nonatomic) SPDYMetadata *metadata; @property (nonatomic) NSData *data; @property (nonatomic) NSInputStream *dataStream; -@property (nonatomic) NSDictionary *headers; @property (nonatomic, weak) NSURLRequest *request; @property (nonatomic, weak) SPDYProtocol *protocol; @property (nonatomic, weak) SPDYPushStreamManager *pushStreamManager; diff --git a/SPDY/SPDYStream.m b/SPDY/SPDYStream.m index 8bd93bd..48bac09 100644 --- a/SPDY/SPDYStream.m +++ b/SPDY/SPDYStream.m @@ -422,17 +422,21 @@ - (void)mergeHeaders:(NSDictionary *)newHeaders return; } - // See if any headers collide with previous - if ([[NSSet setWithArray:[_headers allKeys]] intersectsSet:[NSSet setWithArray:[newHeaders allKeys]]]) { - NSError *error = SPDY_STREAM_ERROR(SPDYStreamProtocolError, @"received duplicate headers"); - [self abortWithError:error status:SPDY_STREAM_PROTOCOL_ERROR]; - return; - } + if (_headers) { + // See if any headers collide with previous + if ([[NSSet setWithArray:[_headers allKeys]] intersectsSet:[NSSet setWithArray:[newHeaders allKeys]]]) { + NSError *error = SPDY_STREAM_ERROR(SPDYStreamProtocolError, @"received duplicate headers"); + [self abortWithError:error status:SPDY_STREAM_PROTOCOL_ERROR]; + return; + } - // Merge raw headers - NSMutableDictionary *merged = [NSMutableDictionary dictionaryWithDictionary:_headers]; - [merged addEntriesFromDictionary:newHeaders]; - _headers = merged; + // Merge raw headers + NSMutableDictionary *merged = [NSMutableDictionary dictionaryWithDictionary:_headers]; + [merged addEntriesFromDictionary:newHeaders]; + _headers = merged; + } else { + _headers = [newHeaders copy]; + } } - (void)didReceiveResponse From 77150cb1681436d77ee87059c805a6065b5be220 Mon Sep 17 00:00:00 2001 From: Kevin Goodier Date: Thu, 8 Oct 2015 23:24:48 -0700 Subject: [PATCH 6/9] Minor cleanup and renaming. --- SPDY/SPDYPushStreamManager.h | 2 +- SPDY/SPDYPushStreamManager.m | 54 +++++++++++------------ SPDY/SPDYStream.m | 20 +++------ SPDYUnitTests/SPDYPushStreamManagerTest.m | 4 +- 4 files changed, 36 insertions(+), 44 deletions(-) diff --git a/SPDY/SPDYPushStreamManager.h b/SPDY/SPDYPushStreamManager.h index 54cbf16..617adfc 100644 --- a/SPDY/SPDYPushStreamManager.h +++ b/SPDY/SPDYPushStreamManager.h @@ -19,7 +19,7 @@ - (NSUInteger)pushStreamCount; - (NSUInteger)associatedStreamCount; - (SPDYStream *)streamForProtocol:(SPDYProtocol *)protocol; -- (void)addStream:(SPDYStream *)stream associatedWith:(SPDYStream *)associatedStream; +- (void)addStream:(SPDYStream *)stream associatedWithStream:(SPDYStream *)associatedStream; - (void)stopLoadingStream:(SPDYStream *)stream; @end diff --git a/SPDY/SPDYPushStreamManager.m b/SPDY/SPDYPushStreamManager.m index bd0e6b7..b2a2ab3 100644 --- a/SPDY/SPDYPushStreamManager.m +++ b/SPDY/SPDYPushStreamManager.m @@ -71,13 +71,13 @@ - (SPDYStream *)attachStreamToProtocol:(SPDYProtocol *)protocol if (_response) { SPDY_DEBUG(@"PUSH.%u: replaying didReceiveResponse: %@", _stream.streamId, _response); [protocol.client URLProtocol:protocol didReceiveResponse:_response cacheStoragePolicy:_cacheStoragePolicy]; - } - if (_data.length > 0) { - SPDY_DEBUG(@"PUSH.%u: replaying didLoadData: %zd bytes", _stream.streamId, _data.length); - [protocol.client URLProtocol:protocol didLoadData:_data]; + if (_data.length > 0) { + SPDY_DEBUG(@"PUSH.%u: replaying didLoadData: %zd bytes", _stream.streamId, _data.length); + [protocol.client URLProtocol:protocol didLoadData:_data]; + } } - + if (_error) { SPDY_DEBUG(@"PUSH.%u: replaying didFailWithError: %@", _stream.streamId, _error); [protocol.client URLProtocol:protocol didFailWithError:_error]; @@ -146,30 +146,30 @@ - (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:( @implementation SPDYPushStreamManager { - NSMapTable *_streamToNodeDictionary; - NSMapTable *_urlToNodeDictionary; - NSMapTable *_associatedStreamToNodeArrayDictionary; + NSMapTable *_streamToNodeMap; + NSMapTable *_urlToNodeMap; + NSMapTable *_associatedStreamToNodeArrayMap; } - (id)init { self = [super init]; if (self) { - _streamToNodeDictionary = [NSMapTable strongToStrongObjectsMapTable]; - _urlToNodeDictionary = [NSMapTable strongToStrongObjectsMapTable]; - _associatedStreamToNodeArrayDictionary = [NSMapTable strongToStrongObjectsMapTable]; + _streamToNodeMap = [NSMapTable strongToStrongObjectsMapTable]; + _urlToNodeMap = [NSMapTable strongToStrongObjectsMapTable]; + _associatedStreamToNodeArrayMap = [NSMapTable strongToStrongObjectsMapTable]; } return self; } - (NSUInteger)pushStreamCount { - return _streamToNodeDictionary.count; + return _streamToNodeMap.count; } - (NSUInteger)associatedStreamCount { - return _associatedStreamToNodeArrayDictionary.count; + return _associatedStreamToNodeArrayMap.count; } - (SPDYStream *)streamForProtocol:(SPDYProtocol *)protocol @@ -177,7 +177,7 @@ - (SPDYStream *)streamForProtocol:(SPDYProtocol *)protocol // @@@ This lookup in our "cache" is only based on the URL. Is there more we need to take // into account? NSURL *requestURL = protocol.request.URL; // protocol.request has already been canonicalized - SPDYPushStreamNode *node = [_urlToNodeDictionary objectForKey:requestURL]; + SPDYPushStreamNode *node = [_urlToNodeMap objectForKey:requestURL]; if (node == nil) { return nil; @@ -189,7 +189,7 @@ - (SPDYStream *)streamForProtocol:(SPDYProtocol *)protocol return [node attachStreamToProtocol:protocol]; } -- (void)addStream:(SPDYStream *)stream associatedWith:(SPDYStream *)associatedStream +- (void)addStream:(SPDYStream *)stream associatedWithStream:(SPDYStream *)associatedStream { SPDY_INFO(@"PUSH.%u: adding stream (%@) associated with stream %u (%@)", stream.streamId, stream.request.URL, associatedStream.streamId, associatedStream.request.URL); @@ -200,22 +200,22 @@ - (void)addStream:(SPDYStream *)stream associatedWith:(SPDYStream *)associatedSt SPDYPushStreamNode *node = [[SPDYPushStreamNode alloc] initWithStream:stream associatedStream:associatedStream]; // In the event a stream with matching request already exists, the new one wins. - [_streamToNodeDictionary setObject:node forKey:stream]; + [_streamToNodeMap setObject:node forKey:stream]; // Add mapping from original request to push requests. Allows us to cancel all push // requests if the original request is cancelled. if (associatedStream) { NSAssert(associatedStream.local, @"associated stream must be local"); - if ([_associatedStreamToNodeArrayDictionary objectForKey:associatedStream] == nil) { - [_associatedStreamToNodeArrayDictionary setObject:[[NSMutableArray alloc] initWithObjects:node, nil] forKey:associatedStream]; + if ([_associatedStreamToNodeArrayMap objectForKey:associatedStream] == nil) { + [_associatedStreamToNodeArrayMap setObject:[[NSMutableArray alloc] initWithObjects:node, nil] forKey:associatedStream]; } else { - [[_associatedStreamToNodeArrayDictionary objectForKey:associatedStream] addObject:node]; + [[_associatedStreamToNodeArrayMap objectForKey:associatedStream] addObject:node]; } } // Add mapping from URL to node to provide cache lookups NSAssert(stream.request, @"push stream must have a request object"); - [_urlToNodeDictionary setObject:node forKey:stream.request.URL]; + [_urlToNodeMap setObject:node forKey:stream.request.URL]; } - (void)stopLoadingStream:(SPDYStream *)stream @@ -227,7 +227,7 @@ - (void)stopLoadingStream:(SPDYStream *)stream // [remote, closed] Remove stream only if its associated local stream has been removed, or if it failed. if (stream.local) { // Make copy because removeStream will mutate the underlying array - NSArray *pushNodes = [[_associatedStreamToNodeArrayDictionary objectForKey:stream] copy]; + NSArray *pushNodes = [[_associatedStreamToNodeArrayMap objectForKey:stream] copy]; if (pushNodes.count > 0) { for (SPDYPushStreamNode *pushNode in pushNodes) { if (!stream.closed) { @@ -255,7 +255,7 @@ - (void)stopLoadingStream:(SPDYStream *)stream // around while the associated stream is open could lead to leaks if the app never // issues requests that hook up to the pushed streams. SPDYStream *associatedStream = stream.associatedStream; // get strong reference - BOOL hasAssociatedStream = (associatedStream && [_associatedStreamToNodeArrayDictionary objectForKey:associatedStream]); + BOOL hasAssociatedStream = (associatedStream && [_associatedStreamToNodeArrayMap objectForKey:associatedStream]); if (!hasAssociatedStream) { SPDY_DEBUG(@"PUSH.%u: removing pushed stream", stream.streamId); [self removeStream:stream]; @@ -272,17 +272,17 @@ - (void)removeStream:(SPDYStream *)stream } if (stream.local) { - [_associatedStreamToNodeArrayDictionary removeObjectForKey:stream]; + [_associatedStreamToNodeArrayMap removeObjectForKey:stream]; } else { - SPDYPushStreamNode *pushNode = [_streamToNodeDictionary objectForKey:stream]; - [_streamToNodeDictionary removeObjectForKey:stream]; + SPDYPushStreamNode *pushNode = [_streamToNodeMap objectForKey:stream]; + [_streamToNodeMap removeObjectForKey:stream]; if (stream.request != nil) { - [_urlToNodeDictionary removeObjectForKey:stream.request.URL]; + [_urlToNodeMap removeObjectForKey:stream.request.URL]; } // Remove the stream from the list of streams related to associated (original) stream. NSAssert(pushNode.associatedStream, @"push stream must have associated stream"); - NSMutableArray *associatedNodes = [_associatedStreamToNodeArrayDictionary objectForKey:pushNode.associatedStream]; + NSMutableArray *associatedNodes = [_associatedStreamToNodeArrayMap objectForKey:pushNode.associatedStream]; for (NSUInteger i = 0; i < associatedNodes.count; i++) { SPDYPushStreamNode *node = associatedNodes[i]; if (node.stream == stream) { diff --git a/SPDY/SPDYStream.m b/SPDY/SPDYStream.m index 48bac09..b16916b 100644 --- a/SPDY/SPDYStream.m +++ b/SPDY/SPDYStream.m @@ -332,9 +332,7 @@ - (void)_close [self markUnblocked]; // just in case. safe if already unblocked. _metadata.blockedMs = _blockedElapsed * 1000; - if (_client) { - [_client URLProtocolDidFinishLoading:_protocol]; - } + [_client URLProtocolDidFinishLoading:_protocol]; if (_delegate && [_delegate respondsToSelector:@selector(streamClosed:)]) { [_delegate streamClosed:self]; @@ -582,9 +580,7 @@ - (void)didReceiveResponse redirect.SPDYBodyStream = nil; } - if (_client) { - [_client URLProtocol:_protocol wasRedirectedToRequest:redirect redirectResponse:_response]; - } + [_client URLProtocol:_protocol wasRedirectedToRequest:redirect redirectResponse:_response]; return; } @@ -649,12 +645,12 @@ - (void)didReceivePushRequest timeoutInterval:_request.timeoutInterval]; requestCopy.allHTTPHeaderFields = _request.allHTTPHeaderFields; requestCopy.HTTPMethod = @"GET"; - requestCopy.SPDYPriority = (NSUInteger)_priority; // TODO: same or +1 (lower priority)? + requestCopy.SPDYPriority = (NSUInteger)_priority; _pushRequest = [SPDYProtocol canonicalRequestForRequest:requestCopy]; _request = _pushRequest; // need a strong reference for _request's weak one - [_pushStreamManager addStream:self associatedWith:_associatedStream]; + [_pushStreamManager addStream:self associatedWithStream:_associatedStream]; // Fire global notification on current thread [[NSNotificationCenter defaultCenter] postNotificationName:SPDYPushRequestReceivedNotification @@ -693,9 +689,7 @@ - (void)didLoadData:(NSData *)data NSUInteger inflatedLength = DECOMPRESSED_CHUNK_LENGTH - _zlibStream.avail_out; inflatedData.length = inflatedLength; if (inflatedLength > 0) { - if (_client) { - [_client URLProtocol:_protocol didLoadData:inflatedData]; - } + [_client URLProtocol:_protocol didLoadData:inflatedData]; } // This can happen if the decompressed data is size N * DECOMPRESSED_CHUNK_LENGTH, @@ -717,9 +711,7 @@ - (void)didLoadData:(NSData *)data } } else { NSData *dataCopy = [[NSData alloc] initWithBytes:data.bytes length:dataLength]; - if (_client) { - [_client URLProtocol:_protocol didLoadData:dataCopy]; - } + [_client URLProtocol:_protocol didLoadData:dataCopy]; } } diff --git a/SPDYUnitTests/SPDYPushStreamManagerTest.m b/SPDYUnitTests/SPDYPushStreamManagerTest.m index 1e39671..3e250c5 100644 --- a/SPDYUnitTests/SPDYPushStreamManagerTest.m +++ b/SPDYUnitTests/SPDYPushStreamManagerTest.m @@ -52,8 +52,8 @@ - (void)_addTwoPushStreams _pushStream2.client = nil; _pushStream2.associatedStream = _associatedStream; - [_pushStreamManager addStream:_pushStream1 associatedWith:_associatedStream]; - [_pushStreamManager addStream:_pushStream2 associatedWith:_associatedStream]; + [_pushStreamManager addStream:_pushStream1 associatedWithStream:_associatedStream]; + [_pushStreamManager addStream:_pushStream2 associatedWithStream:_associatedStream]; XCTAssertEqual(_pushStreamManager.pushStreamCount, 2U); XCTAssertEqual(_pushStreamManager.associatedStreamCount, 1U); From 7dc3e22c92095fccc910c3365b3a9263029bbc89 Mon Sep 17 00:00:00 2001 From: Kevin Goodier Date: Thu, 8 Oct 2015 23:25:12 -0700 Subject: [PATCH 7/9] Also use User-Agent for key in push stream manager cache. --- SPDY/SPDYPushStreamManager.m | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/SPDY/SPDYPushStreamManager.m b/SPDY/SPDYPushStreamManager.m index b2a2ab3..0d4bf41 100644 --- a/SPDY/SPDYPushStreamManager.m +++ b/SPDY/SPDYPushStreamManager.m @@ -20,6 +20,17 @@ #error "This file requires ARC support." #endif +@implementation NSURLRequest (SPDYPushStreamManager) + +- (NSString *)keyForMemoryCache +{ + NSString *urlString = self.URL.absoluteString; + NSString *userAgent = [self valueForHTTPHeaderField:@"user-agent"]; + return [NSString stringWithFormat:@"%@__%@", urlString, userAgent]; +} + +@end + @interface SPDYPushStreamManager () - (void)removeStream:(SPDYStream *)stream; @end @@ -77,7 +88,7 @@ - (SPDYStream *)attachStreamToProtocol:(SPDYProtocol *)protocol [protocol.client URLProtocol:protocol didLoadData:_data]; } } - + if (_error) { SPDY_DEBUG(@"PUSH.%u: replaying didFailWithError: %@", _stream.streamId, _error); [protocol.client URLProtocol:protocol didFailWithError:_error]; @@ -147,7 +158,7 @@ - (void)URLProtocol:(NSURLProtocol *)protocol didCancelAuthenticationChallenge:( @implementation SPDYPushStreamManager { NSMapTable *_streamToNodeMap; - NSMapTable *_urlToNodeMap; + NSMapTable *_requestToNodeMap; NSMapTable *_associatedStreamToNodeArrayMap; } @@ -156,7 +167,7 @@ - (id)init self = [super init]; if (self) { _streamToNodeMap = [NSMapTable strongToStrongObjectsMapTable]; - _urlToNodeMap = [NSMapTable strongToStrongObjectsMapTable]; + _requestToNodeMap = [NSMapTable strongToStrongObjectsMapTable]; _associatedStreamToNodeArrayMap = [NSMapTable strongToStrongObjectsMapTable]; } return self; @@ -174,10 +185,9 @@ - (NSUInteger)associatedStreamCount - (SPDYStream *)streamForProtocol:(SPDYProtocol *)protocol { - // @@@ This lookup in our "cache" is only based on the URL. Is there more we need to take - // into account? - NSURL *requestURL = protocol.request.URL; // protocol.request has already been canonicalized - SPDYPushStreamNode *node = [_urlToNodeMap objectForKey:requestURL]; + // This lookup in our "cache" is only based on the URL and user agent. It is not quite the + // same as a standard HTTP cache lookup. protocol.request has already been canonicalized. + SPDYPushStreamNode *node = [_requestToNodeMap objectForKey:[protocol.request keyForMemoryCache]]; if (node == nil) { return nil; @@ -185,7 +195,7 @@ - (SPDYStream *)streamForProtocol:(SPDYProtocol *)protocol [self removeStream:node.stream]; - SPDY_DEBUG(@"PUSH.%u: attaching push stream to protocol for request %@", node.stream.streamId, requestURL); + SPDY_DEBUG(@"PUSH.%u: attaching push stream to protocol for request %@", node.stream.streamId, protocol.request.URL); return [node attachStreamToProtocol:protocol]; } @@ -215,7 +225,7 @@ - (void)addStream:(SPDYStream *)stream associatedWithStream:(SPDYStream *)associ // Add mapping from URL to node to provide cache lookups NSAssert(stream.request, @"push stream must have a request object"); - [_urlToNodeMap setObject:node forKey:stream.request.URL]; + [_requestToNodeMap setObject:node forKey:[stream.request keyForMemoryCache]]; } - (void)stopLoadingStream:(SPDYStream *)stream @@ -277,7 +287,7 @@ - (void)removeStream:(SPDYStream *)stream SPDYPushStreamNode *pushNode = [_streamToNodeMap objectForKey:stream]; [_streamToNodeMap removeObjectForKey:stream]; if (stream.request != nil) { - [_urlToNodeMap removeObjectForKey:stream.request.URL]; + [_requestToNodeMap removeObjectForKey:[stream.request keyForMemoryCache]]; } // Remove the stream from the list of streams related to associated (original) stream. From f0858eff5c4ae4b38925b447bf9cdb532cc43f3c Mon Sep 17 00:00:00 2001 From: Kevin Goodier Date: Mon, 26 Oct 2015 15:32:49 -0700 Subject: [PATCH 8/9] Fix SPDYProtocolContext with SPDYPushStreamManager. Incorporating SPDYPushStreamManager broke SPDYProtocolContext - it was never getting a stream / metadata associated with it. Added unit test. --- SPDY.xcodeproj/project.pbxproj | 12 +++-- SPDY/SPDYProtocol.m | 16 +++--- SPDYUnitTests/SPDYProtocolContextTest.m | 67 +++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 SPDYUnitTests/SPDYProtocolContextTest.m diff --git a/SPDY.xcodeproj/project.pbxproj b/SPDY.xcodeproj/project.pbxproj index 3c931a6..7609579 100644 --- a/SPDY.xcodeproj/project.pbxproj +++ b/SPDY.xcodeproj/project.pbxproj @@ -106,10 +106,10 @@ 5C5EA4701A119B630058FB64 /* SPDYOriginEndpoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5EA46A1A119B630058FB64 /* SPDYOriginEndpoint.m */; }; 5C5EA4731A119C950058FB64 /* SPDYMockOriginEndpointManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5EA4721A119C950058FB64 /* SPDYMockOriginEndpointManager.m */; }; 5C5EA4751A119CAB0058FB64 /* SPDYSocketTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C5EA4741A119CAB0058FB64 /* SPDYSocketTest.m */; }; - 5C6D809A1BC44C19003AF2E0 /* SPDYURLCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D80991BC44C19003AF2E0 /* SPDYURLCacheTest.m */; settings = {ASSET_TAGS = (); }; }; - 5C6D80AB1BC457B3003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D80A91BC457A9003AF2E0 /* SPDYCanonicalRequest.m */; settings = {ASSET_TAGS = (); }; }; - 5C6D80AC1BC457B4003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D80A91BC457A9003AF2E0 /* SPDYCanonicalRequest.m */; settings = {ASSET_TAGS = (); }; }; - 5C6D80AD1BC457B5003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D80A91BC457A9003AF2E0 /* SPDYCanonicalRequest.m */; settings = {ASSET_TAGS = (); }; }; + 5C6D809A1BC44C19003AF2E0 /* SPDYURLCacheTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D80991BC44C19003AF2E0 /* SPDYURLCacheTest.m */; }; + 5C6D80AB1BC457B3003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D80A91BC457A9003AF2E0 /* SPDYCanonicalRequest.m */; }; + 5C6D80AC1BC457B4003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D80A91BC457A9003AF2E0 /* SPDYCanonicalRequest.m */; }; + 5C6D80AD1BC457B5003AF2E0 /* SPDYCanonicalRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C6D80A91BC457A9003AF2E0 /* SPDYCanonicalRequest.m */; }; 5C750B501A390C7200CC0F2F /* SPDYPushStreamManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C750B4F1A390C7200CC0F2F /* SPDYPushStreamManagerTest.m */; }; 5C7E662F1B0533360037AD91 /* SPDYProtocolTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C7E662E1B0533360037AD91 /* SPDYProtocolTest.m */; }; 5C8089701A266C5700CAC4FF /* SPDYPushStreamManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 5C80896D1A266C5700CAC4FF /* SPDYPushStreamManager.h */; }; @@ -120,6 +120,7 @@ 5C9A0BD11A363BDC00CF2D3D /* SPDYOriginEndpointManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A0BCF1A363BDC00CF2D3D /* SPDYOriginEndpointManager.m */; }; 5C9A0BD21A363BDC00CF2D3D /* SPDYOriginEndpointManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C9A0BCF1A363BDC00CF2D3D /* SPDYOriginEndpointManager.m */; }; 5CA0B9C81A6486F10068ABD9 /* SPDYSettingsStoreTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CA0B9C71A6486F10068ABD9 /* SPDYSettingsStoreTest.m */; }; + 5CC7B9051BDECD43006E2952 /* SPDYProtocolContextTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CC7B9041BDECD43006E2952 /* SPDYProtocolContextTest.m */; }; 5CE43CE11AD74FC900E73FAC /* SPDYMetadata+Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CE43CDF1AD74E2200E73FAC /* SPDYMetadata+Utils.m */; }; 5CE43CE21AD74FC900E73FAC /* SPDYMetadata+Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CE43CDF1AD74E2200E73FAC /* SPDYMetadata+Utils.m */; }; 5CE43CE31AD74FCA00E73FAC /* SPDYMetadata+Utils.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CE43CDF1AD74E2200E73FAC /* SPDYMetadata+Utils.m */; }; @@ -212,6 +213,7 @@ 5C9A0BCB1A363B7700CF2D3D /* SPDYOriginEndpointManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SPDYOriginEndpointManager.h; sourceTree = ""; }; 5C9A0BCF1A363BDC00CF2D3D /* SPDYOriginEndpointManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYOriginEndpointManager.m; sourceTree = ""; }; 5CA0B9C71A6486F10068ABD9 /* SPDYSettingsStoreTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYSettingsStoreTest.m; sourceTree = ""; }; + 5CC7B9041BDECD43006E2952 /* SPDYProtocolContextTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYProtocolContextTest.m; sourceTree = ""; }; 5CE43CDE1AD74E2200E73FAC /* SPDYMetadata+Utils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SPDYMetadata+Utils.h"; sourceTree = ""; }; 5CE43CDF1AD74E2200E73FAC /* SPDYMetadata+Utils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SPDYMetadata+Utils.m"; sourceTree = ""; }; 5CF0A2C81A089BC500B6D141 /* SPDYMetadataTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SPDYMetadataTest.m; sourceTree = ""; }; @@ -300,6 +302,7 @@ 5CF0A2C81A089BC500B6D141 /* SPDYMetadataTest.m */, 5C04570419B043CB009E0AC2 /* SPDYOriginEndpointTest.m */, 0679F3CE186217FC006F122E /* SPDYOriginTest.m */, + 5CC7B9041BDECD43006E2952 /* SPDYProtocolContextTest.m */, 5C7E662E1B0533360037AD91 /* SPDYProtocolTest.m */, 5C750B4F1A390C7200CC0F2F /* SPDYPushStreamManagerTest.m */, 7774C803F0948788139CD2C1 /* SPDYServerPushTest.m */, @@ -695,6 +698,7 @@ 5C5EA4751A119CAB0058FB64 /* SPDYSocketTest.m in Sources */, 5C2229591952257800CAF160 /* SPDYURLRequestTest.m in Sources */, 5CF0A2CC1A0952D900B6D141 /* SPDYMockURLProtocolClient.m in Sources */, + 5CC7B9051BDECD43006E2952 /* SPDYProtocolContextTest.m in Sources */, 064EFB2F1671638A002F0AEC /* SPDYMockFrameDecoderDelegate.m in Sources */, 5C5EA4731A119C950058FB64 /* SPDYMockOriginEndpointManager.m in Sources */, 069D0E8B167F9D010037D8AF /* SPDYStream.m in Sources */, diff --git a/SPDY/SPDYProtocol.m b/SPDY/SPDYProtocol.m index a3147df..12abd57 100644 --- a/SPDY/SPDYProtocol.m +++ b/SPDY/SPDYProtocol.m @@ -51,6 +51,7 @@ @interface SPDYAssertionHandler : NSAssertionHandler @end @interface SPDYProtocolContext : NSObject +- (void)associateWithStream:(SPDYStream *)stream; @end @implementation SPDYAssertionHandler @@ -122,15 +123,13 @@ @implementation SPDYProtocolContext SPDYMetadata *_metadata; } -- (instancetype)initWithStream:(SPDYStream *)stream +- (void)associateWithStream:(SPDYStream *)stream { - self = [super init]; - if (self) { - _metadata = stream.metadata; - } - return self; + _metadata = stream.metadata; } +#pragma mark SPDYProtocolContext protocol + - (SPDYMetadata *)metadata { return _metadata; @@ -385,8 +384,8 @@ - (void)startLoading } // Create the context, but delay stream creation (to allow for looking up push cache - // as late as possible) - _context = [[SPDYProtocolContext alloc] initWithStream:_stream]; + // as late as possible). Must associate stream with this instance of the context then. + _context = [[SPDYProtocolContext alloc] init]; if (request.SPDYURLSession) { [self detectSessionAndTaskThenContinueWithOrigin:origin]; @@ -457,6 +456,7 @@ - (void)startStreamForOrigin:(SPDYOrigin *)origin _stream = [[SPDYStream alloc] initWithProtocol:self pushStreamManager:manager.pushStreamManager]; [manager queueStream:_stream]; } + [_context associateWithStream:_stream]; } - (void)stopLoading diff --git a/SPDYUnitTests/SPDYProtocolContextTest.m b/SPDYUnitTests/SPDYProtocolContextTest.m new file mode 100644 index 0000000..cee5a01 --- /dev/null +++ b/SPDYUnitTests/SPDYProtocolContextTest.m @@ -0,0 +1,67 @@ +// +// SPDYProtocolContextTest.m +// SPDY +// +// Created by Kevin Goodier on 10/26/15. +// Copyright © 2015 Twitter. All rights reserved. +// + +#import +#import +#import "NSURLRequest+SPDYURLRequest.h" +#import "SPDYProtocol.h" +#import "SPDYTLSTrustEvaluator.h" + +@interface SPDYProtocolContextTest : XCTestCase +@end + +@implementation SPDYProtocolContextTest +{ + id _spdyContext; +} + +- (void)tearDown +{ + _spdyContext = nil; +} + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didStartLoadingRequest:(NSURLRequest *)request withContext:(id)context +{ + _spdyContext = context; +} + +- (void)testSPDYProtocolContextDoesProvideMetadata +{ + NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; + sessionConfig.protocolClasses = @[ [SPDYURLSessionProtocol class] ]; + sessionConfig.timeoutIntervalForRequest = 1.0; + NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:[NSOperationQueue currentQueue]]; + + // Need a bogus endpoint that will fail quickly + // TODO: FUTURE WORK mock SPDYSocket to avoid all network activity (and this hack) + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://127.0.0.1:12345/foo"]]; + request.SPDYURLSession = session; + + BOOL __block taskComplete; + NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + taskComplete = YES; + CFRunLoopStop(CFRunLoopGetCurrent()); + }]; + XCTAssertNotNil(task); + [task resume]; + + CFAbsoluteTime timeout = CFAbsoluteTimeGetCurrent() + 5.0; + while (!taskComplete && CFAbsoluteTimeGetCurrent() < timeout) { + CFRunLoopRun(); + } + XCTAssertTrue(taskComplete); + + XCTAssertNotNil(_spdyContext, @"URLSession:task:didStartLoadingRequest:withContext delegate not called"); + + SPDYMetadata *metadata = [_spdyContext metadata]; + XCTAssertNotNil(metadata); + XCTAssertEqualObjects(metadata.version, @"3.1"); +} + +@end + From 0eff9df07747a399e87790fa1db48ccff6d7cf85 Mon Sep 17 00:00:00 2001 From: Kevin Goodier Date: Mon, 26 Oct 2015 16:37:42 -0700 Subject: [PATCH 9/9] Improve mergeHeaders in SPDYStream. --- SPDY/SPDYStream.m | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/SPDY/SPDYStream.m b/SPDY/SPDYStream.m index b16916b..c6fd637 100644 --- a/SPDY/SPDYStream.m +++ b/SPDY/SPDYStream.m @@ -421,16 +421,16 @@ - (void)mergeHeaders:(NSDictionary *)newHeaders } if (_headers) { - // See if any headers collide with previous - if ([[NSSet setWithArray:[_headers allKeys]] intersectsSet:[NSSet setWithArray:[newHeaders allKeys]]]) { - NSError *error = SPDY_STREAM_ERROR(SPDYStreamProtocolError, @"received duplicate headers"); - [self abortWithError:error status:SPDY_STREAM_PROTOCOL_ERROR]; - return; - } - - // Merge raw headers NSMutableDictionary *merged = [NSMutableDictionary dictionaryWithDictionary:_headers]; - [merged addEntriesFromDictionary:newHeaders]; + for (NSString *newKey in newHeaders) { + NSString *newValue = newHeaders[newKey]; + if (nil != merged[newKey]) { + NSError *error = SPDY_STREAM_ERROR(SPDYStreamProtocolError, @"received duplicate headers"); + [self abortWithError:error status:SPDY_STREAM_PROTOCOL_ERROR]; + return; + } + merged[newKey] = newValue; + } _headers = merged; } else { _headers = [newHeaders copy];