diff --git a/SPDY.xcodeproj/project.pbxproj b/SPDY.xcodeproj/project.pbxproj index 8e99104..7609579 100644 --- a/SPDY.xcodeproj/project.pbxproj +++ b/SPDY.xcodeproj/project.pbxproj @@ -98,30 +98,29 @@ 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 = (); }; }; + 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 */; }; + 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 */; }; - 5CA0B9C61A6454950068ABD9 /* SPDYProtocolTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 5CA0B9C51A6454950068ABD9 /* SPDYProtocolTest.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 */; }; @@ -129,12 +128,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,34 +196,39 @@ 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 = ""; }; + 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 = ""; }; - 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 = ""; }; + 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 = ""; }; 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 +302,10 @@ 5CF0A2C81A089BC500B6D141 /* SPDYMetadataTest.m */, 5C04570419B043CB009E0AC2 /* SPDYOriginEndpointTest.m */, 0679F3CE186217FC006F122E /* SPDYOriginTest.m */, - 5CA0B9C51A6454950068ABD9 /* SPDYProtocolTest.m */, + 5CC7B9041BDECD43006E2952 /* SPDYProtocolContextTest.m */, + 5C7E662E1B0533360037AD91 /* SPDYProtocolTest.m */, + 5C750B4F1A390C7200CC0F2F /* SPDYPushStreamManagerTest.m */, + 7774C803F0948788139CD2C1 /* SPDYServerPushTest.m */, 25959A3E1937DE3900FC9731 /* SPDYSessionManagerTest.m */, 7774C2026A4DE9957D75F629 /* SPDYSessionTest.m */, 5CA0B9C71A6486F10068ABD9 /* SPDYSettingsStoreTest.m */, @@ -323,10 +332,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 +399,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 */, + 5C6D80A81BC457A9003AF2E0 /* SPDYCanonicalRequest.h */, + 5C6D80A91BC457A9003AF2E0 /* SPDYCanonicalRequest.m */, 062EA63E175D4CD3003BC1CE /* SPDYCommonLogger.h */, 062EA63F175D4CD3003BC1CE /* SPDYCommonLogger.m */, 062EA63C175D1A15003BC1CE /* SPDYDefinitions.h */, @@ -420,6 +429,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 */, @@ -457,10 +468,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 +483,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; }; @@ -490,11 +497,10 @@ 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 */, - 5C6B0D2B1A3A3E8400334BFA /* SPDYCanonicalRequest.h in Headers */, 062EA640175D4CD3003BC1CE /* SPDYCommonLogger.h in Headers */, 7774CF887055793F373F0D5E /* SPDYStopwatch.h in Headers */, - 5C48CF841B0A65DD0082F7EF /* SPDYCacheStoragePolicy.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -593,9 +599,9 @@ projectDirPath = ""; projectRoot = ""; targets = ( - 064EFB1216715C9F002F0AEC /* SPDYUnitTests */, 0651EBE716F3F7C700CE44D2 /* SPDY.iphoneos */, 0651EC0716F3F7E500CE44D2 /* SPDY.macosx */, + 064EFB1216715C9F002F0AEC /* SPDYUnitTests */, 0652631216F7B6360081868F /* SPDY */, ); }; @@ -672,28 +678,27 @@ 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 */, + 5C750B501A390C7200CC0F2F /* SPDYPushStreamManagerTest.m in Sources */, 06FDA20F16717DF100137DBD /* SPDYSession.m in Sources */, 06FDA21116717DF100137DBD /* SPDYSessionManager.m in Sources */, 5CF0A2C91A089BC500B6D141 /* SPDYMetadataTest.m in Sources */, 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 */, 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 */, @@ -706,14 +711,19 @@ 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 */, 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 */, 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 */, 7774CD12A73EA9ABAE521441 /* SPDYStopwatch.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -726,13 +736,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 */, - 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 */, + 5C6D80AD1BC457B5003AF2E0 /* SPDYCanonicalRequest.m in Sources */, 0651EC4316F3FA1400CE44D2 /* SPDYSession.m in Sources */, 0651EC4416F3FA1400CE44D2 /* SPDYSessionManager.m in Sources */, 0651EC4516F3FA1400CE44D2 /* SPDYSettingsStore.m in Sources */, @@ -742,6 +752,7 @@ 5CE43CE21AD74FC900E73FAC /* SPDYMetadata+Utils.m in Sources */, 061C8E9617C5954400D22083 /* SPDYStreamManager.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 */, @@ -756,13 +767,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 */, - 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 */, + 5C6D80AC1BC457B4003AF2E0 /* SPDYCanonicalRequest.m in Sources */, 0651EC2916F3FA0B00CE44D2 /* SPDYSession.m in Sources */, 0651EC2A16F3FA0B00CE44D2 /* SPDYSessionManager.m in Sources */, 0651EC2B16F3FA0B00CE44D2 /* SPDYSettingsStore.m in Sources */, @@ -772,6 +783,7 @@ 5CE43CE31AD74FCA00E73FAC /* SPDYMetadata+Utils.m in Sources */, 061C8E9817C5954400D22083 /* SPDYStreamManager.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 +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)"; @@ -859,7 +871,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 +1040,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/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 b7884ad..12abd57 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" @@ -32,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"; @@ -49,6 +51,7 @@ @interface SPDYAssertionHandler : NSAssertionHandler @end @interface SPDYProtocolContext : NSObject +- (void)associateWithStream:(SPDYStream *)stream; @end @implementation SPDYAssertionHandler @@ -120,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; @@ -382,16 +383,14 @@ - (void)startLoading origin = aliasedOrigin; } - // Create the stream - _stream = [[SPDYStream alloc] initWithProtocol:self]; - _context = [[SPDYProtocolContext alloc] initWithStream:_stream]; + // Create the context, but delay stream creation (to allow for looking up push cache + // as late as possible). Must associate stream with this instance of the context then. + _context = [[SPDYProtocolContext alloc] init]; 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,27 @@ - (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]; + } + [_context associateWithStream:_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..617adfc --- /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 associatedWithStream:(SPDYStream *)associatedStream; +- (void)stopLoadingStream:(SPDYStream *)stream; + +@end diff --git a/SPDY/SPDYPushStreamManager.m b/SPDY/SPDYPushStreamManager.m new file mode 100644 index 0000000..0d4bf41 --- /dev/null +++ b/SPDY/SPDYPushStreamManager.m @@ -0,0 +1,306 @@ +// +// 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 + +@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 + +@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 *_streamToNodeMap; + NSMapTable *_requestToNodeMap; + NSMapTable *_associatedStreamToNodeArrayMap; +} + +- (id)init +{ + self = [super init]; + if (self) { + _streamToNodeMap = [NSMapTable strongToStrongObjectsMapTable]; + _requestToNodeMap = [NSMapTable strongToStrongObjectsMapTable]; + _associatedStreamToNodeArrayMap = [NSMapTable strongToStrongObjectsMapTable]; + } + return self; +} + +- (NSUInteger)pushStreamCount +{ + return _streamToNodeMap.count; +} + +- (NSUInteger)associatedStreamCount +{ + return _associatedStreamToNodeArrayMap.count; +} + +- (SPDYStream *)streamForProtocol:(SPDYProtocol *)protocol +{ + // 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; + } + + [self removeStream:node.stream]; + + SPDY_DEBUG(@"PUSH.%u: attaching push stream to protocol for request %@", node.stream.streamId, protocol.request.URL); + return [node attachStreamToProtocol:protocol]; +} + +- (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); + + // 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. + [_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 ([_associatedStreamToNodeArrayMap objectForKey:associatedStream] == nil) { + [_associatedStreamToNodeArrayMap setObject:[[NSMutableArray alloc] initWithObjects:node, nil] forKey:associatedStream]; + } else { + [[_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"); + [_requestToNodeMap setObject:node forKey:[stream.request keyForMemoryCache]]; +} + +- (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 = [[_associatedStreamToNodeArrayMap 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 && [_associatedStreamToNodeArrayMap 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) { + [_associatedStreamToNodeArrayMap removeObjectForKey:stream]; + } else { + SPDYPushStreamNode *pushNode = [_streamToNodeMap objectForKey:stream]; + [_streamToNodeMap removeObjectForKey:stream]; + if (stream.request != nil) { + [_requestToNodeMap removeObjectForKey:[stream.request keyForMemoryCache]]; + } + + // Remove the stream from the list of streams related to associated (original) stream. + NSAssert(pushNode.associatedStream, @"push stream must have associated stream"); + NSMutableArray *associatedNodes = [_associatedStreamToNodeArrayMap 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/SPDYSession.m b/SPDY/SPDYSession.m index dc8b647..b3c42f2 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,46 @@ - (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; + } + + 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 +656,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 +815,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 +822,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/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 ef586e1..4232944 100644 --- a/SPDY/SPDYStream.h +++ b/SPDY/SPDYStream.h @@ -13,6 +13,7 @@ #import "SPDYDefinitions.h" @class SPDYProtocol; +@class SPDYPushStreamManager; @class SPDYMetadata; @class SPDYStream; @@ -32,6 +33,8 @@ @property (nonatomic) NSInputStream *dataStream; @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; @property (nonatomic) bool local; @@ -46,14 +49,17 @@ @property (nonatomic) uint32_t sendWindowSizeLowerBound; @property (nonatomic) uint32_t receiveWindowSizeLowerBound; -- (instancetype)initWithProtocol:(SPDYProtocol *)protocol; +- (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; - (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..c6fd637 100644 --- a/SPDY/SPDYStream.m +++ b/SPDY/SPDYStream.m @@ -17,10 +17,13 @@ #import #import "NSURLRequest+SPDYURLRequest.h" #import "SPDYCacheStoragePolicy.h" +#import "SPDYCanonicalRequest.h" #import "SPDYCommonLogger.h" #import "SPDYDefinitions.h" #import "SPDYMetadata+Utils.h" +#import "SPDYOrigin.h" #import "SPDYProtocol+Project.h" +#import "SPDYPushStreamManager.h" #import "SPDYStopwatch.h" #import "SPDYStream.h" @@ -51,6 +54,7 @@ @implementation SPDYStream NSData *_data; NSString *_dataFile; NSInputStream *_dataStream; + NSDictionary *_headers; NSRunLoop *_runLoop; CFReadStreamRef _dataStreamRef; CFRunLoopRef _runLoopRef; @@ -61,16 +65,22 @@ @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 + 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); @@ -80,8 +90,35 @@ - (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; + _pushStreamManager = associatedStream.pushStreamManager; + _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]; } @@ -295,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]; @@ -374,10 +409,46 @@ - (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; + } + + if (_headers) { + NSMutableDictionary *merged = [NSMutableDictionary dictionaryWithDictionary:_headers]; + 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]; + } +} + +- (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 +459,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 +470,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 +483,7 @@ - (void)didReceiveResponse:(NSDictionary *)headers } } - NSURL *requestURL = _protocol.request.URL; + NSURL *requestURL = _request.URL; BOOL cookiesOn = NO; NSHTTPCookieStorage *cookieStore = nil; @@ -418,7 +491,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 +502,7 @@ - (void)didReceiveResponse:(NSDictionary *)headers break; } } else { - cookiesOn = _protocol.request.HTTPShouldHandleCookies; + cookiesOn = _request.HTTPShouldHandleCookies; cookieStore = [NSHTTPCookieStorage sharedHTTPCookieStorage]; } @@ -448,10 +521,11 @@ - (void)didReceiveResponse:(NSDictionary *)headers [cookieStore setCookies:cookies forURL:requestURL - mainDocumentURL:_protocol.request.mainDocumentURL]; + mainDocumentURL:_request.mainDocumentURL]; } } + // 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) { @@ -461,13 +535,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 +555,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 +580,82 @@ - (void)didReceiveResponse:(NSDictionary *)headers redirect.SPDYBodyStream = nil; } - [_client URLProtocol:_protocol wasRedirectedToRequest:redirect redirectResponse:response]; + [_client URLProtocol:_protocol wasRedirectedToRequest:redirect redirectResponse:_response]; + return; } - NSURLCacheStoragePolicy cachePolicy = SPDYCacheStoragePolicy(_request, response); - [_client URLProtocol:_protocol - didReceiveResponse:response - cacheStoragePolicy:cachePolicy]; + 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; + } + + // 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 + // 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. + 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 + + [_pushStreamManager addStream:self associatedWithStream:_associatedStream]; + + // Fire global notification on current thread + [[NSNotificationCenter defaultCenter] postNotificationName:SPDYPushRequestReceivedNotification + object:nil + userInfo:@{ @"request": _request }]; } - (void)didLoadData:(NSData *)data @@ -522,6 +663,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; diff --git a/SPDYUnitTests/SPDYMockSessionTestBase.h b/SPDYUnitTests/SPDYMockSessionTestBase.h index 8f85e8d..fffee48 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,8 @@ typedef void (^SPDYAsyncTestCallback)(); - (SPDYProtocol *)createProtocol; - (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 9514cce..fcad40d 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" @@ -34,7 +35,7 @@ - (id)init return self; } -- (void)streamCanceled:(SPDYStream *)stream status:(SPDYStreamStatus)status; +- (void)streamCanceled:(SPDYStream *)stream status:(SPDYStreamStatus)status { _calledStreamCanceled++; _lastStream = stream; @@ -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,22 @@ - (void)makeSocketConnect [[_session socket] performDelegateCall_socketDidConnectToHost:@"testhost" port:1234]; } +- (SPDYStream *)attachToPushRequestWithUrl:(NSString *)url +{ + NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:url]]; + return [self attachToPushRequest:request]; +} + +- (SPDYStream *)attachToPushRequest:(NSURLRequest *)request +{ + _mockPushURLProtocolClient = [[SPDYMockURLProtocolClient alloc] init]; + SPDYProtocol *pushProtocolRequest = [[SPDYProtocol alloc] initWithRequest:request 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/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 + diff --git a/SPDYUnitTests/SPDYPushStreamManagerTest.m b/SPDYUnitTests/SPDYPushStreamManagerTest.m new file mode 100644 index 0000000..3e250c5 --- /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 associatedWithStream:_associatedStream]; + [_pushStreamManager addStream:_pushStream2 associatedWithStream:_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 new file mode 100644 index 0000000..6ca976c --- /dev/null +++ b/SPDYUnitTests/SPDYServerPushTest.m @@ -0,0 +1,662 @@ +// +// 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" +#import "SPDYMockURLProtocolClient.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); +} + +- (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 +{ + // 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)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 + [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]]); +} + +- (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 a57deba..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", @@ -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; @@ -397,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", @@ -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; @@ -424,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", @@ -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..3f7c19f 100644 --- a/SPDYUnitTests/SPDYStreamTest.m +++ b/SPDYUnitTests/SPDYStreamTest.m @@ -1,4 +1,3 @@ -// // SPDYStreamTest.m // SPDY // @@ -89,51 +88,72 @@ - (void)testStreamingWithStream XCTAssertEqual(_mockURLProtocolClient.lastError.code, (errorCode)); \ } while (0) +- (void)testMergeHeadersCollisionDoesAbort +{ + SPDYStream *stream = [self createStream]; + [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]]; + SPDYStream *stream = [self createStream]; [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); } - (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", @":status":@"99", @":version":@"http/1.1"}; - [stream didReceiveResponse:headers]; + [stream mergeHeaders:headers]; + [stream didReceiveResponse]; SPDYAssertStreamError(NSURLErrorDomain, NSURLErrorBadServerResponse); } - (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", @":status":@"200"}; - [stream didReceiveResponse:headers]; + + [stream mergeHeaders:headers]; + [stream didReceiveResponse]; SPDYAssertStreamError(NSURLErrorDomain, NSURLErrorBadServerResponse); } - (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", @":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; @@ -153,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", @@ -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); @@ -181,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", @@ -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]); }