diff --git a/Alamofire.xcodeproj/project.pbxproj b/Alamofire.xcodeproj/project.pbxproj index ec9716e9d..9227bb067 100644 --- a/Alamofire.xcodeproj/project.pbxproj +++ b/Alamofire.xcodeproj/project.pbxproj @@ -13,12 +13,46 @@ 4C256A541B096C770065714F /* BaseTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C256A501B096C2C0065714F /* BaseTestCase.swift */; }; 4C3238E71B3604DB00FE04AE /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */; }; 4C3238E81B3604DB00FE04AE /* MultipartFormDataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */; }; - 4C3238EC1B3617BB00FE04AE /* rainbow.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 4C3238EA1B3617BB00FE04AE /* rainbow.jpg */; }; - 4C3238ED1B3617BB00FE04AE /* rainbow.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 4C3238EA1B3617BB00FE04AE /* rainbow.jpg */; }; - 4C3238EE1B3617BB00FE04AE /* unicorn.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C3238EB1B3617BB00FE04AE /* unicorn.png */; }; - 4C3238EF1B3617BB00FE04AE /* unicorn.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C3238EB1B3617BB00FE04AE /* unicorn.png */; }; + 4C33A1391B5207DB00873DFF /* rainbow.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 4C33A1231B5207DB00873DFF /* rainbow.jpg */; }; + 4C33A13A1B5207DB00873DFF /* rainbow.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 4C33A1231B5207DB00873DFF /* rainbow.jpg */; }; + 4C33A13B1B5207DB00873DFF /* unicorn.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C33A1241B5207DB00873DFF /* unicorn.png */; }; + 4C33A13C1B5207DB00873DFF /* unicorn.png in Resources */ = {isa = PBXBuildFile; fileRef = 4C33A1241B5207DB00873DFF /* unicorn.png */; }; + 4C33A1431B52089C00873DFF /* ServerTrustPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C33A1421B52089C00873DFF /* ServerTrustPolicyTests.swift */; }; + 4C33A1441B52089C00873DFF /* ServerTrustPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C33A1421B52089C00873DFF /* ServerTrustPolicyTests.swift */; }; 4C341BBA1B1A865A00C1B34D /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C341BB91B1A865A00C1B34D /* CacheTests.swift */; }; 4C341BBB1B1A865A00C1B34D /* CacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C341BB91B1A865A00C1B34D /* CacheTests.swift */; }; + 4C811F8D1B51856D00E0F59A /* ServerTrustPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C811F8C1B51856D00E0F59A /* ServerTrustPolicy.swift */; }; + 4C811F8E1B51856D00E0F59A /* ServerTrustPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C811F8C1B51856D00E0F59A /* ServerTrustPolicy.swift */; }; + 4C812C3B1B535F220017E0BF /* alamofire-root-ca.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C3A1B535F220017E0BF /* alamofire-root-ca.cer */; }; + 4C812C3C1B535F220017E0BF /* alamofire-root-ca.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C3A1B535F220017E0BF /* alamofire-root-ca.cer */; }; + 4C812C3F1B535F2E0017E0BF /* alamofire-signing-ca1.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C3D1B535F2E0017E0BF /* alamofire-signing-ca1.cer */; }; + 4C812C401B535F2E0017E0BF /* alamofire-signing-ca1.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C3D1B535F2E0017E0BF /* alamofire-signing-ca1.cer */; }; + 4C812C411B535F2E0017E0BF /* alamofire-signing-ca2.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C3E1B535F2E0017E0BF /* alamofire-signing-ca2.cer */; }; + 4C812C421B535F2E0017E0BF /* alamofire-signing-ca2.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C3E1B535F2E0017E0BF /* alamofire-signing-ca2.cer */; }; + 4C812C471B535F400017E0BF /* *.alamofire.org.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C431B535F400017E0BF /* *.alamofire.org.cer */; }; + 4C812C481B535F400017E0BF /* *.alamofire.org.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C431B535F400017E0BF /* *.alamofire.org.cer */; }; + 4C812C491B535F400017E0BF /* multiple-dns-names.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C441B535F400017E0BF /* multiple-dns-names.cer */; }; + 4C812C4A1B535F400017E0BF /* multiple-dns-names.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C441B535F400017E0BF /* multiple-dns-names.cer */; }; + 4C812C4B1B535F400017E0BF /* signed-by-ca1.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C451B535F400017E0BF /* signed-by-ca1.cer */; }; + 4C812C4C1B535F400017E0BF /* signed-by-ca1.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C451B535F400017E0BF /* signed-by-ca1.cer */; }; + 4C812C4D1B535F400017E0BF /* test.alamofire.org.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C461B535F400017E0BF /* test.alamofire.org.cer */; }; + 4C812C4E1B535F400017E0BF /* test.alamofire.org.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C461B535F400017E0BF /* test.alamofire.org.cer */; }; + 4C812C541B535F540017E0BF /* expired.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C4F1B535F540017E0BF /* expired.cer */; }; + 4C812C551B535F540017E0BF /* expired.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C4F1B535F540017E0BF /* expired.cer */; }; + 4C812C561B535F540017E0BF /* missing-dns-name-and-uri.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C501B535F540017E0BF /* missing-dns-name-and-uri.cer */; }; + 4C812C571B535F540017E0BF /* missing-dns-name-and-uri.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C501B535F540017E0BF /* missing-dns-name-and-uri.cer */; }; + 4C812C581B535F540017E0BF /* signed-by-ca2.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C511B535F540017E0BF /* signed-by-ca2.cer */; }; + 4C812C591B535F540017E0BF /* signed-by-ca2.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C511B535F540017E0BF /* signed-by-ca2.cer */; }; + 4C812C5A1B535F540017E0BF /* valid-dns-name.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C521B535F540017E0BF /* valid-dns-name.cer */; }; + 4C812C5B1B535F540017E0BF /* valid-dns-name.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C521B535F540017E0BF /* valid-dns-name.cer */; }; + 4C812C5C1B535F540017E0BF /* valid-uri.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C531B535F540017E0BF /* valid-uri.cer */; }; + 4C812C5D1B535F540017E0BF /* valid-uri.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C531B535F540017E0BF /* valid-uri.cer */; }; + 4C812C611B535F6D0017E0BF /* intermediate-ca-disig.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C5E1B535F6D0017E0BF /* intermediate-ca-disig.cer */; }; + 4C812C621B535F6D0017E0BF /* intermediate-ca-disig.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C5E1B535F6D0017E0BF /* intermediate-ca-disig.cer */; }; + 4C812C631B535F6D0017E0BF /* root-ca-disig.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C5F1B535F6D0017E0BF /* root-ca-disig.cer */; }; + 4C812C641B535F6D0017E0BF /* root-ca-disig.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C5F1B535F6D0017E0BF /* root-ca-disig.cer */; }; + 4C812C651B535F6D0017E0BF /* testssl-expire.disig.sk.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C601B535F6D0017E0BF /* testssl-expire.disig.sk.cer */; }; + 4C812C661B535F6D0017E0BF /* testssl-expire.disig.sk.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4C812C601B535F6D0017E0BF /* testssl-expire.disig.sk.cer */; }; 4CCFA79A1B2BE71600B6F460 /* URLProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */; }; 4CCFA79B1B2BE71600B6F460 /* URLProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */; }; 4CDE2C371AF8932A00BABAE5 /* Manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDE2C361AF8932A00BABAE5 /* Manager.swift */; }; @@ -81,9 +115,26 @@ 4C23EB421B327C5B0090E0BC /* MultipartFormData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartFormData.swift; sourceTree = ""; }; 4C256A501B096C2C0065714F /* BaseTestCase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTestCase.swift; sourceTree = ""; }; 4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipartFormDataTests.swift; sourceTree = ""; }; - 4C3238EA1B3617BB00FE04AE /* rainbow.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; name = rainbow.jpg; path = Resources/rainbow.jpg; sourceTree = ""; }; - 4C3238EB1B3617BB00FE04AE /* unicorn.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = unicorn.png; path = Resources/unicorn.png; sourceTree = ""; }; + 4C33A1231B5207DB00873DFF /* rainbow.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = rainbow.jpg; sourceTree = ""; }; + 4C33A1241B5207DB00873DFF /* unicorn.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = unicorn.png; sourceTree = ""; }; + 4C33A1421B52089C00873DFF /* ServerTrustPolicyTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerTrustPolicyTests.swift; sourceTree = ""; }; 4C341BB91B1A865A00C1B34D /* CacheTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheTests.swift; sourceTree = ""; }; + 4C811F8C1B51856D00E0F59A /* ServerTrustPolicy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerTrustPolicy.swift; sourceTree = ""; }; + 4C812C3A1B535F220017E0BF /* alamofire-root-ca.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "alamofire-root-ca.cer"; path = "alamofire.org/alamofire-root-ca.cer"; sourceTree = ""; }; + 4C812C3D1B535F2E0017E0BF /* alamofire-signing-ca1.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "alamofire-signing-ca1.cer"; path = "alamofire.org/alamofire-signing-ca1.cer"; sourceTree = ""; }; + 4C812C3E1B535F2E0017E0BF /* alamofire-signing-ca2.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "alamofire-signing-ca2.cer"; path = "alamofire.org/alamofire-signing-ca2.cer"; sourceTree = ""; }; + 4C812C431B535F400017E0BF /* *.alamofire.org.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "*.alamofire.org.cer"; path = "alamofire.org/*.alamofire.org.cer"; sourceTree = ""; }; + 4C812C441B535F400017E0BF /* multiple-dns-names.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "multiple-dns-names.cer"; path = "alamofire.org/multiple-dns-names.cer"; sourceTree = ""; }; + 4C812C451B535F400017E0BF /* signed-by-ca1.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "signed-by-ca1.cer"; path = "alamofire.org/signed-by-ca1.cer"; sourceTree = ""; }; + 4C812C461B535F400017E0BF /* test.alamofire.org.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = test.alamofire.org.cer; path = alamofire.org/test.alamofire.org.cer; sourceTree = ""; }; + 4C812C4F1B535F540017E0BF /* expired.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = expired.cer; path = alamofire.org/expired.cer; sourceTree = ""; }; + 4C812C501B535F540017E0BF /* missing-dns-name-and-uri.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "missing-dns-name-and-uri.cer"; path = "alamofire.org/missing-dns-name-and-uri.cer"; sourceTree = ""; }; + 4C812C511B535F540017E0BF /* signed-by-ca2.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "signed-by-ca2.cer"; path = "alamofire.org/signed-by-ca2.cer"; sourceTree = ""; }; + 4C812C521B535F540017E0BF /* valid-dns-name.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "valid-dns-name.cer"; path = "alamofire.org/valid-dns-name.cer"; sourceTree = ""; }; + 4C812C531B535F540017E0BF /* valid-uri.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "valid-uri.cer"; path = "alamofire.org/valid-uri.cer"; sourceTree = ""; }; + 4C812C5E1B535F6D0017E0BF /* intermediate-ca-disig.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "intermediate-ca-disig.cer"; path = "disig.sk/intermediate-ca-disig.cer"; sourceTree = ""; }; + 4C812C5F1B535F6D0017E0BF /* root-ca-disig.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "root-ca-disig.cer"; path = "disig.sk/root-ca-disig.cer"; sourceTree = ""; }; + 4C812C601B535F6D0017E0BF /* testssl-expire.disig.sk.cer */ = {isa = PBXFileReference; lastKnownFileType = file; name = "testssl-expire.disig.sk.cer"; path = "disig.sk/testssl-expire.disig.sk.cer"; sourceTree = ""; }; 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLProtocolTests.swift; sourceTree = ""; }; 4CDE2C361AF8932A00BABAE5 /* Manager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Manager.swift; sourceTree = ""; }; 4CDE2C391AF899EC00BABAE5 /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; @@ -162,6 +213,7 @@ 4C341BB91B1A865A00C1B34D /* CacheTests.swift */, F8111E5B19A9674D0040E7D1 /* DownloadTests.swift */, 4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */, + 4C33A1421B52089C00873DFF /* ServerTrustPolicyTests.swift */, F86AEFE51AE6A282007D9C76 /* TLSEvaluationTests.swift */, F8111E5F19A9674D0040E7D1 /* UploadTests.swift */, 4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */, @@ -173,12 +225,101 @@ 4C3238E91B3617A600FE04AE /* Resources */ = { isa = PBXGroup; children = ( - 4C3238EA1B3617BB00FE04AE /* rainbow.jpg */, - 4C3238EB1B3617BB00FE04AE /* unicorn.png */, + 4C33A1171B5207DB00873DFF /* Certificates */, + 4C33A1221B5207DB00873DFF /* Images */, ); name = Resources; sourceTree = ""; }; + 4C33A1171B5207DB00873DFF /* Certificates */ = { + isa = PBXGroup; + children = ( + 4C812C391B535F060017E0BF /* alamofire.org */, + 4C812C381B535F000017E0BF /* disig.sk */, + ); + name = Certificates; + path = Resources/Certificates; + sourceTree = ""; + }; + 4C33A1221B5207DB00873DFF /* Images */ = { + isa = PBXGroup; + children = ( + 4C33A1231B5207DB00873DFF /* rainbow.jpg */, + 4C33A1241B5207DB00873DFF /* unicorn.png */, + ); + name = Images; + path = Resources/Images; + sourceTree = ""; + }; + 4C33A13D1B52080800873DFF /* Root */ = { + isa = PBXGroup; + children = ( + 4C812C3A1B535F220017E0BF /* alamofire-root-ca.cer */, + ); + name = Root; + sourceTree = ""; + }; + 4C33A13E1B52081100873DFF /* Intermediate */ = { + isa = PBXGroup; + children = ( + 4C812C3D1B535F2E0017E0BF /* alamofire-signing-ca1.cer */, + 4C812C3E1B535F2E0017E0BF /* alamofire-signing-ca2.cer */, + ); + name = Intermediate; + sourceTree = ""; + }; + 4C33A13F1B52081A00873DFF /* Leaf */ = { + isa = PBXGroup; + children = ( + 4C33A1401B52084400873DFF /* Signed by CA1 */, + 4C33A1411B52084E00873DFF /* Signed by CA2 */, + ); + name = Leaf; + sourceTree = ""; + }; + 4C33A1401B52084400873DFF /* Signed by CA1 */ = { + isa = PBXGroup; + children = ( + 4C812C431B535F400017E0BF /* *.alamofire.org.cer */, + 4C812C441B535F400017E0BF /* multiple-dns-names.cer */, + 4C812C451B535F400017E0BF /* signed-by-ca1.cer */, + 4C812C461B535F400017E0BF /* test.alamofire.org.cer */, + ); + name = "Signed by CA1"; + sourceTree = ""; + }; + 4C33A1411B52084E00873DFF /* Signed by CA2 */ = { + isa = PBXGroup; + children = ( + 4C812C4F1B535F540017E0BF /* expired.cer */, + 4C812C501B535F540017E0BF /* missing-dns-name-and-uri.cer */, + 4C812C511B535F540017E0BF /* signed-by-ca2.cer */, + 4C812C521B535F540017E0BF /* valid-dns-name.cer */, + 4C812C531B535F540017E0BF /* valid-uri.cer */, + ); + name = "Signed by CA2"; + sourceTree = ""; + }; + 4C812C381B535F000017E0BF /* disig.sk */ = { + isa = PBXGroup; + children = ( + 4C812C5E1B535F6D0017E0BF /* intermediate-ca-disig.cer */, + 4C812C5F1B535F6D0017E0BF /* root-ca-disig.cer */, + 4C812C601B535F6D0017E0BF /* testssl-expire.disig.sk.cer */, + ); + name = disig.sk; + sourceTree = ""; + }; + 4C812C391B535F060017E0BF /* alamofire.org */ = { + isa = PBXGroup; + children = ( + 4C33A13D1B52080800873DFF /* Root */, + 4C33A13E1B52081100873DFF /* Intermediate */, + 4C33A13F1B52081A00873DFF /* Leaf */, + ); + name = alamofire.org; + sourceTree = ""; + }; 4CDE2C481AF8A14A00BABAE5 /* Core */ = { isa = PBXGroup; children = ( @@ -195,6 +336,7 @@ 4CDE2C3C1AF89D4900BABAE5 /* Download.swift */, 4C23EB421B327C5B0090E0BC /* MultipartFormData.swift */, 4CDE2C451AF89FF300BABAE5 /* ResponseSerialization.swift */, + 4C811F8C1B51856D00E0F59A /* ServerTrustPolicy.swift */, 4CDE2C3F1AF89E0700BABAE5 /* Upload.swift */, 4CDE2C421AF89F0900BABAE5 /* Validation.swift */, ); @@ -417,8 +559,23 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4C3238EE1B3617BB00FE04AE /* unicorn.png in Resources */, - 4C3238EC1B3617BB00FE04AE /* rainbow.jpg in Resources */, + 4C812C3B1B535F220017E0BF /* alamofire-root-ca.cer in Resources */, + 4C812C4B1B535F400017E0BF /* signed-by-ca1.cer in Resources */, + 4C812C541B535F540017E0BF /* expired.cer in Resources */, + 4C812C491B535F400017E0BF /* multiple-dns-names.cer in Resources */, + 4C812C4D1B535F400017E0BF /* test.alamofire.org.cer in Resources */, + 4C812C411B535F2E0017E0BF /* alamofire-signing-ca2.cer in Resources */, + 4C33A13B1B5207DB00873DFF /* unicorn.png in Resources */, + 4C812C561B535F540017E0BF /* missing-dns-name-and-uri.cer in Resources */, + 4C812C581B535F540017E0BF /* signed-by-ca2.cer in Resources */, + 4C812C651B535F6D0017E0BF /* testssl-expire.disig.sk.cer in Resources */, + 4C812C611B535F6D0017E0BF /* intermediate-ca-disig.cer in Resources */, + 4C33A1391B5207DB00873DFF /* rainbow.jpg in Resources */, + 4C812C631B535F6D0017E0BF /* root-ca-disig.cer in Resources */, + 4C812C5C1B535F540017E0BF /* valid-uri.cer in Resources */, + 4C812C3F1B535F2E0017E0BF /* alamofire-signing-ca1.cer in Resources */, + 4C812C5A1B535F540017E0BF /* valid-dns-name.cer in Resources */, + 4C812C471B535F400017E0BF /* *.alamofire.org.cer in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -426,8 +583,23 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 4C3238EF1B3617BB00FE04AE /* unicorn.png in Resources */, - 4C3238ED1B3617BB00FE04AE /* rainbow.jpg in Resources */, + 4C812C3C1B535F220017E0BF /* alamofire-root-ca.cer in Resources */, + 4C812C4C1B535F400017E0BF /* signed-by-ca1.cer in Resources */, + 4C812C551B535F540017E0BF /* expired.cer in Resources */, + 4C812C4A1B535F400017E0BF /* multiple-dns-names.cer in Resources */, + 4C812C4E1B535F400017E0BF /* test.alamofire.org.cer in Resources */, + 4C812C421B535F2E0017E0BF /* alamofire-signing-ca2.cer in Resources */, + 4C33A13C1B5207DB00873DFF /* unicorn.png in Resources */, + 4C812C571B535F540017E0BF /* missing-dns-name-and-uri.cer in Resources */, + 4C812C591B535F540017E0BF /* signed-by-ca2.cer in Resources */, + 4C812C661B535F6D0017E0BF /* testssl-expire.disig.sk.cer in Resources */, + 4C812C621B535F6D0017E0BF /* intermediate-ca-disig.cer in Resources */, + 4C33A13A1B5207DB00873DFF /* rainbow.jpg in Resources */, + 4C812C641B535F6D0017E0BF /* root-ca-disig.cer in Resources */, + 4C812C5D1B535F540017E0BF /* valid-uri.cer in Resources */, + 4C812C401B535F2E0017E0BF /* alamofire-signing-ca1.cer in Resources */, + 4C812C5B1B535F540017E0BF /* valid-dns-name.cer in Resources */, + 4C812C481B535F400017E0BF /* *.alamofire.org.cer in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -445,6 +617,7 @@ 4CDE2C381AF8932A00BABAE5 /* Manager.swift in Sources */, 4DD67C251A5C590000ED2280 /* Alamofire.swift in Sources */, 4C23EB441B327C5B0090E0BC /* MultipartFormData.swift in Sources */, + 4C811F8E1B51856D00E0F59A /* ServerTrustPolicy.swift in Sources */, 4CDE2C3E1AF89D4900BABAE5 /* Download.swift in Sources */, 4CDE2C441AF89F0900BABAE5 /* Validation.swift in Sources */, ); @@ -461,6 +634,7 @@ 4CDE2C371AF8932A00BABAE5 /* Manager.swift in Sources */, F897FF4119AA800700AB5182 /* Alamofire.swift in Sources */, 4C23EB431B327C5B0090E0BC /* MultipartFormData.swift in Sources */, + 4C811F8D1B51856D00E0F59A /* ServerTrustPolicy.swift in Sources */, 4CDE2C3D1AF89D4900BABAE5 /* Download.swift in Sources */, 4CDE2C431AF89F0900BABAE5 /* Validation.swift in Sources */, ); @@ -471,6 +645,7 @@ buildActionMask = 2147483647; files = ( 4C3238E71B3604DB00FE04AE /* MultipartFormDataTests.swift in Sources */, + 4C33A1431B52089C00873DFF /* ServerTrustPolicyTests.swift in Sources */, 4C341BBA1B1A865A00C1B34D /* CacheTests.swift in Sources */, 4CCFA79A1B2BE71600B6F460 /* URLProtocolTests.swift in Sources */, F86AEFE71AE6A312007D9C76 /* TLSEvaluationTests.swift in Sources */, @@ -491,6 +666,7 @@ buildActionMask = 2147483647; files = ( 4C3238E81B3604DB00FE04AE /* MultipartFormDataTests.swift in Sources */, + 4C33A1441B52089C00873DFF /* ServerTrustPolicyTests.swift in Sources */, 4C341BBB1B1A865A00C1B34D /* CacheTests.swift in Sources */, 4CCFA79B1B2BE71600B6F460 /* URLProtocolTests.swift in Sources */, F829C6BE1A7A950600A2CD59 /* ParameterEncodingTests.swift in Sources */, diff --git a/README.md b/README.md index 2538c86bd..e8e9d320c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,10 @@ [![Build Status](https://travis-ci.org/Alamofire/Alamofire.svg)](https://travis-ci.org/Alamofire/Alamofire) [![Cocoapods Compatible](https://img.shields.io/cocoapods/v/Alamofire.svg)](https://img.shields.io/cocoapods/v/Alamofire.svg) -[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) +[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) +[![License](https://img.shields.io/cocoapods/l/Alamofire.svg?style=flat&color=gray)](http://cocoadocs.org/docsets/Alamofire) +[![Platform](https://img.shields.io/cocoapods/p/Alamofire.svg?style=flat)](http://cocoadocs.org/docsets/Alamofire) +[![Twitter](https://img.shields.io/badge/twitter-@AlamofireSF-blue.svg?style=flat)](http://twitter.com/AlamofireSF) Alamofire is an HTTP networking library written in Swift. @@ -293,7 +296,7 @@ Adding a custom HTTP header to a `Request` is supported directly in the global ` ```swift let headers = [ - "Authorization", "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", + "Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", "Content-Type": "application/x-www-form-urlencoded" ] @@ -882,6 +885,59 @@ enum Router: URLRequestConvertible { Alamofire.request(Router.ReadUser("mattt")) // GET /users/mattt ``` +### Security + +Using a secure HTTPS connection when communicating with servers and web services is an important step in securing sensitive data. By default, Alamofire will evaluate the certificate chain provided by the server using Apple's built in validation provided by the Security framework. While this guarantees the certificate chain is valid, it does not prevent man-in-the-middle (MITM) attacks or other potential vulnerabilities. In order to mitigate MITM attacks, applications dealing with sensitive customer data or financial information should use certificate or public key pinning provided by the `ServerTrustPolicy`. + +#### ServerTrustPolicy + +The `ServerTrustPolicy` enumeration evaluates the server trust generally provided by an `NSURLAuthenticationChallenge` when connecting to a server over a secure HTTPS connection. + +```swift +let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: ServerTrustPolicy.certificatesInBundle(), + validateCertificateChain: true, + validateHost: true +) +``` + +There are many different cases of server trust evaluation giving you complete control over the validation process: + +* `PerformDefaultEvaluation`: Uses the default server trust evaluation while allowing you to control whether to validate the host provided by the challenge. +* `PinCertificates`: Uses the pinned certificates to validate the server trust. The server trust is considered valid if one of the pinned certificates match one of the server certificates. +* `PinPublicKeys`: Uses the pinned public keys to validate the server trust. The server trust is considered valid if one of the pinned public keys match one of the server certificate public keys. +* `DisableEvaluation`: Disables all evaluation which in turn will always consider any server trust as valid. +* `CustomEvaluation`: Uses the associated closure to evaluate the validity of the server trust thus giving you complete control over the validation process. Use with caution. + +#### Server Trust Policy Manager + +The `ServerTrustPolicyManager` is responsible for storing an internal mapping of server trust policies to a particular host. This allows Alamofire to evaluate each host against a different server trust policy. + +```swift +let serverTrustPolicies: [String: ServerTrustPolicy] = [ + "test.example.com": .PinCertificates( + certificates: ServerTrustPolicy.certificatesInBundle(), + validateCertificateChain: true, + validateHost: true + ), + "insecure.expired-apis.com": .DisableEvaluation +] + +let manager = Manager( + configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), + serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies) +) +``` + +These server trust policies will result in the following behavior: + +* `test.example.com` will always use certificate pinning with certificate chain and host validation enabled thus requiring the following criteria to be met to allow the TLS handshake to succeed: + * Certificate chain MUST be valid. + * Certificate chain MUST include one of the pinned certificates. + * Challenge host MUST match the host in the certificate chain's leaf certificate. +* `insecure.expired-apis.com` will never evaluate the certificate chain and will always allow the TLS handshake to succeed. +* All other hosts will use the default evaluation provided by Apple. + * * * ## FAQ @@ -899,8 +955,6 @@ AFNetworking remains the premiere networking library available for OS X and iOS, Use AFNetworking for any of the following: - UIKit extensions, such as asynchronously loading images to `UIImageView` -- TLS verification, using `AFSecurityManager` -- Situations requiring `NSOperation` or `NSURLConnection`, using `AFURLConnectionOperation` - Network reachability monitoring, using `AFNetworkReachabilityManager` ### What's the origin of the name Alamofire? @@ -911,7 +965,11 @@ Alamofire is named after the [Alamo Fire flower](https://aggie-horticulture.tamu ## Credits -Alamofire is owned and maintained by the [Alamofire Software Foundation](http://alamofire.org). +Alamofire is owned and maintained by the [Alamofire Software Foundation](http://alamofire.org). You can follow them on Twitter at [@AlamofireSF](https://twitter.com/AlamofireSF) for project updates and releases. + +### Security Disclosure + +If you believe you have identified a security vulnerability with Alamofire, you should report it as soon as possible via email to security@alamofire.org. Please do not post it to a public issue tracker. ## License diff --git a/Source/Download.swift b/Source/Download.swift index 6b0e8dbdf..871c9a225 100644 --- a/Source/Download.swift +++ b/Source/Download.swift @@ -43,11 +43,13 @@ extension Manager { } let request = Request(session: self.session, task: downloadTask) + if let downloadDelegate = request.delegate as? Request.DownloadTaskDelegate { downloadDelegate.downloadTaskDidFinishDownloadingToURL = { session, downloadTask, URL in return destination(URL, downloadTask.response as! NSHTTPURLResponse) } } + self.delegate[request.delegate.task] = request.delegate if self.startRequestsImmediately { @@ -132,6 +134,17 @@ extension Request { } } + /// The resume data of the underlying download task if available after a failure. + public var resumeData: NSData? { + var data: NSData? + + if let delegate = self.delegate as? DownloadTaskDelegate { + data = delegate.resumeData + } + + return data + } + // MARK: - DownloadTaskDelegate class DownloadTaskDelegate: TaskDelegate, NSURLSessionDownloadDelegate { @@ -139,7 +152,7 @@ extension Request { var downloadProgress: ((Int64, Int64, Int64) -> Void)? var resumeData: NSData? - override var data: NSData? { return resumeData } + override var data: NSData? { return self.resumeData } // MARK: - NSURLSessionDownloadDelegate diff --git a/Source/Manager.swift b/Source/Manager.swift index 401cac514..117b44aaf 100644 --- a/Source/Manager.swift +++ b/Source/Manager.swift @@ -72,8 +72,9 @@ public class Manager { var mutableUserAgent = NSMutableString(string: "\(executable)/\(bundle) (\(version); OS \(os))") as CFMutableString let transform = NSString(string: "Any-Latin; Latin-ASCII; [:^ASCII:] Remove") as CFString + if CFStringTransform(mutableUserAgent, nil, transform, 0) == 1 { - return mutableUserAgent as NSString as! String + return mutableUserAgent as String } } @@ -104,11 +105,15 @@ public class Manager { // MARK: - Lifecycle /** - :param: configuration The configuration used to construct the managed session. + Initializes the Manager instance with the given configuration and server trust policy. + + :param: configuration The configuration used to construct the managed session. `nil` by default. + :param: serverTrustPolicyManager The server trust policy manager to use for evaluating all server trust challenges. `nil` by default. */ - required public init(configuration: NSURLSessionConfiguration? = nil) { + required public init(configuration: NSURLSessionConfiguration? = nil, serverTrustPolicyManager: ServerTrustPolicyManager? = nil) { self.delegate = SessionDelegate() self.session = NSURLSession(configuration: configuration, delegate: delegate, delegateQueue: nil) + self.session.serverTrustPolicyManager = serverTrustPolicyManager self.delegate.sessionDidFinishEventsForBackgroundURLSession = { [weak self] session in if let strongSelf = self { @@ -157,13 +162,14 @@ public class Manager { :returns: The created request. */ public func request(URLRequest: URLRequestConvertible) -> Request { - var dataTask: NSURLSessionDataTask? + var dataTask: NSURLSessionDataTask! + dispatch_sync(self.queue) { dataTask = self.session.dataTaskWithRequest(URLRequest.URLRequest) } - let request = Request(session: self.session, task: dataTask!) - self.delegate[request.delegate.task] = request.delegate + let request = Request(session: self.session, task: dataTask) + delegate[request.delegate.task] = request.delegate if self.startRequestsImmediately { request.resume() @@ -218,11 +224,28 @@ public class Manager { } public func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: ((NSURLSessionAuthChallengeDisposition, NSURLCredential!) -> Void)) { + var disposition: NSURLSessionAuthChallengeDisposition = .PerformDefaultHandling + var credential: NSURLCredential! + if let sessionDidReceiveChallenge = self.sessionDidReceiveChallenge { - completionHandler(sessionDidReceiveChallenge(session, challenge)) - } else { - completionHandler(.PerformDefaultHandling, nil) + (disposition, credential) = sessionDidReceiveChallenge(session, challenge) + } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + let host = challenge.protectionSpace.host + + if let + serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicyForHost(host), + serverTrust = challenge.protectionSpace.serverTrust + { + if serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) { + disposition = .UseCredential + credential = NSURLCredential(forTrust: serverTrust) + } else { + disposition = .CancelAuthenticationChallenge + } + } } + + completionHandler(disposition, credential) } public func URLSessionDidFinishEventsForBackgroundURLSession(session: NSURLSession) { @@ -291,7 +314,6 @@ public class Manager { taskDidComplete(session, task, error) } else if let delegate = self[task] { delegate.URLSession(session, task: task, didCompleteWithError: error) - self[task] = nil } } diff --git a/Source/MultipartFormData.swift b/Source/MultipartFormData.swift index 82c016bb4..2818d172f 100644 --- a/Source/MultipartFormData.swift +++ b/Source/MultipartFormData.swift @@ -109,6 +109,7 @@ public class MultipartFormData { public let boundary: String private var bodyParts: [BodyPart] + private var bodyPartError: NSError? private let streamBufferSize: Int // MARK: - Lifecycle @@ -133,6 +134,71 @@ public class MultipartFormData { // MARK: - Body Parts + /** + Creates a body part from the data and appends it to the multipart form data object. + + The body part data will be encoded using the following format: + + - `Content-Disposition: form-data; name=#{name}` (HTTP Header) + - Encoded data + - Multipart form boundary + + :param: data The data to encode into the multipart form data. + :param: name The name to associate with the data in the `Content-Disposition` HTTP header. + */ + public func appendBodyPart(#data: NSData, name: String) { + let headers = contentHeaders(name: name) + let stream = NSInputStream(data: data) + let length = UInt64(data.length) + + appendBodyPart(stream: stream, length: length, headers: headers) + } + + /** + Creates a body part from the data and appends it to the multipart form data object. + + The body part data will be encoded using the following format: + + - `Content-Disposition: form-data; name=#{name}` (HTTP Header) + - `Content-Type: #{generated mimeType}` (HTTP Header) + - Encoded data + - Multipart form boundary + + :param: data The data to encode into the multipart form data. + :param: name The name to associate with the data in the `Content-Disposition` HTTP header. + :param: mimeType The MIME type to associate with the data content type in the `Content-Type` HTTP header. + */ + public func appendBodyPart(#data: NSData, name: String, mimeType: String) { + let headers = contentHeaders(name: name, mimeType: mimeType) + let stream = NSInputStream(data: data) + let length = UInt64(data.length) + + appendBodyPart(stream: stream, length: length, headers: headers) + } + + /** + Creates a body part from the data and appends it to the multipart form data object. + + The body part data will be encoded using the following format: + + - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header) + - `Content-Type: #{mimeType}` (HTTP Header) + - Encoded file data + - Multipart form boundary + + :param: data The data to encode into the multipart form data. + :param: name The name to associate with the data in the `Content-Disposition` HTTP header. + :param: fileName The filename to associate with the data in the `Content-Disposition` HTTP header. + :param: mimeType The MIME type to associate with the data in the `Content-Type` HTTP header. + */ + public func appendBodyPart(#data: NSData, name: String, fileName: String, mimeType: String) { + let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType) + let stream = NSInputStream(data: data) + let length = UInt64(data.length) + + appendBodyPart(stream: stream, length: length, headers: headers) + } + /** Creates a body part from the file and appends it to the multipart form data object. @@ -147,24 +213,23 @@ public class MultipartFormData { `fileURL`. The `Content-Type` HTTP header MIME type is generated by mapping the `fileURL` extension to the system associated MIME type. - :param: URL The URL of the file whose content will be encoded into the multipart form data. - :param: name The name to associate with the file content in the `Content-Disposition` HTTP header. - - :returns: An `NSError` if an error occurred, `nil` otherwise. + :param: fileURL The URL of the file whose content will be encoded into the multipart form data. + :param: name The name to associate with the file content in the `Content-Disposition` HTTP header. */ - public func appendBodyPart(fileURL URL: NSURL, name: String) -> NSError? { + public func appendBodyPart(#fileURL: NSURL, name: String) { if let - fileName = URL.lastPathComponent, - pathExtension = URL.pathExtension + fileName = fileURL.lastPathComponent, + pathExtension = fileURL.pathExtension { let mimeType = mimeTypeForPathExtension(pathExtension) - return appendBodyPart(fileURL: URL, name: name, fileName: fileName, mimeType: mimeType) - } - - let failureReason = "Failed to extract the fileName of the provided URL: \(URL)" - let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason] + appendBodyPart(fileURL: fileURL, name: name, fileName: fileName, mimeType: mimeType) + } else { + let failureReason = "Failed to extract the fileName of the provided URL: \(fileURL)" + let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason] + let error = NSError(domain: AlamofireErrorDomain, code: NSURLErrorBadURL, userInfo: userInfo) - return NSError(domain: AlamofireErrorDomain, code: NSURLErrorBadURL, userInfo: userInfo) + setBodyPartError(error) + } } /** @@ -177,117 +242,96 @@ public class MultipartFormData { - Encoded file data - Multipart form boundary - :param: URL The URL of the file whose content will be encoded into the multipart form data. + :param: fileURL The URL of the file whose content will be encoded into the multipart form data. :param: name The name to associate with the file content in the `Content-Disposition` HTTP header. :param: fileName The filename to associate with the file content in the `Content-Disposition` HTTP header. :param: mimeType The MIME type to associate with the file content in the `Content-Type` HTTP header. - - :returns: An `NSError` if an error occurred, `nil` otherwise. */ - public func appendBodyPart(fileURL URL: NSURL, name: String, fileName: String, mimeType: String) -> NSError? { + public func appendBodyPart(#fileURL: NSURL, name: String, fileName: String, mimeType: String) { let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType) var isDirectory: ObjCBool = false + var error: NSError? - if !URL.fileURL { - return errorWithCode(NSURLErrorBadURL, failureReason: "The URL does not point to a file URL: \(URL)") - } else if !URL.checkResourceIsReachableAndReturnError(nil) { - return errorWithCode(NSURLErrorBadURL, failureReason: "The URL is not reachable: \(URL)") - } else if NSFileManager.defaultManager().fileExistsAtPath(URL.path!, isDirectory: &isDirectory) && isDirectory { - return errorWithCode(NSURLErrorBadURL, failureReason: "The URL is a directory, not a file: \(URL)") + if !fileURL.fileURL { + error = errorWithCode(NSURLErrorBadURL, failureReason: "The URL does not point to a file URL: \(fileURL)") + } else if !fileURL.checkResourceIsReachableAndReturnError(nil) { + error = errorWithCode(NSURLErrorBadURL, failureReason: "The URL is not reachable: \(fileURL)") + } else if NSFileManager.defaultManager().fileExistsAtPath(fileURL.path!, isDirectory: &isDirectory) && isDirectory { + error = errorWithCode(NSURLErrorBadURL, failureReason: "The URL is a directory, not a file: \(fileURL)") } - let bodyContentLength: UInt64 - var fileAttributesError: NSError? + if let error = error { + setBodyPartError(error) + return + } + + let length: UInt64 if let - path = URL.path, - attributes = NSFileManager.defaultManager().attributesOfItemAtPath(path, error: &fileAttributesError), + path = fileURL.path, + attributes = NSFileManager.defaultManager().attributesOfItemAtPath(path, error: &error), fileSize = (attributes[NSFileSize] as? NSNumber)?.unsignedLongLongValue { - bodyContentLength = fileSize + length = fileSize } else { - return errorWithCode(NSURLErrorBadURL, failureReason: "Could not fetch attributes from the URL: \(URL)") + let failureReason = "Could not fetch attributes from the URL: \(fileURL)" + let error = errorWithCode(NSURLErrorBadURL, failureReason: failureReason) + + setBodyPartError(error) + + return } - if let bodyStream = NSInputStream(URL: URL) { - let bodyPart = BodyPart(headers: headers, bodyStream: bodyStream, bodyContentLength: bodyContentLength) - self.bodyParts.append(bodyPart) + if let stream = NSInputStream(URL: fileURL) { + appendBodyPart(stream: stream, length: length, headers: headers) } else { - let failureReason = "Failed to create an input stream from the URL: \(URL)" - return errorWithCode(NSURLErrorCannotOpenFile, failureReason: failureReason) - } + let failureReason = "Failed to create an input stream from the URL: \(fileURL)" + let error = errorWithCode(NSURLErrorCannotOpenFile, failureReason: failureReason) - return nil + setBodyPartError(error) + } } /** - Creates a body part from the data and appends it to the multipart form data object. + Creates a body part from the stream and appends it to the multipart form data object. The body part data will be encoded using the following format: - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header) - `Content-Type: #{mimeType}` (HTTP Header) - - Encoded file data + - Encoded stream data - Multipart form boundary - :param: data The data to encode into the multipart form data. - :param: name The name to associate with the data in the `Content-Disposition` HTTP header. - :param: fileName The filename to associate with the data in the `Content-Disposition` HTTP header. - :param: mimeType The MIME type to associate with the data in the `Content-Type` HTTP header. + :param: stream The input stream to encode in the multipart form data. + :param: length The content length of the stream. + :param: name The name to associate with the stream content in the `Content-Disposition` HTTP header. + :param: fileName The filename to associate with the stream content in the `Content-Disposition` HTTP header. + :param: mimeType The MIME type to associate with the stream content in the `Content-Type` HTTP header. */ - public func appendBodyPart(fileData data: NSData, name: String, fileName: String, mimeType: String) { + public func appendBodyPart(#stream: NSInputStream, length: UInt64, name: String, fileName: String, mimeType: String) { let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType) - let bodyStream = NSInputStream(data: data) - let bodyContentLength = UInt64(data.length) - let bodyPart = BodyPart(headers: headers, bodyStream: bodyStream, bodyContentLength: bodyContentLength) - - self.bodyParts.append(bodyPart) - } - - /** - Creates a body part from the data and appends it to the multipart form data object. - - The body part data will be encoded using the following format: - - - `Content-Disposition: form-data; name=#{name}` (HTTP Header) - - Encoded file data - - Multipart form boundary - - :param: data The data to encode into the multipart form data. - :param: name The name to associate with the data in the `Content-Disposition` HTTP header. - */ - public func appendBodyPart(#data: NSData, name: String) { - let headers = contentHeaders(name: name) - let bodyStream = NSInputStream(data: data) - let bodyContentLength = UInt64(data.length) - let bodyPart = BodyPart(headers: headers, bodyStream: bodyStream, bodyContentLength: bodyContentLength) - - self.bodyParts.append(bodyPart) + appendBodyPart(stream: stream, length: length, headers: headers) } /** - Creates a body part from the stream and appends it to the multipart form data object. + Creates a body part with the headers, stream and length and appends it to the multipart form data object. The body part data will be encoded using the following format: - - `Content-Disposition: form-data; name=#{name}; filename=#{filename}` (HTTP Header) - - `Content-Type: #{mimeType}` (HTTP Header) - - Encoded file data + - HTTP headers + - Encoded stream data - Multipart form boundary - :param: stream The input stream to encode in the multipart form data. - :param: name The name to associate with the stream content in the `Content-Disposition` HTTP header. - :param: fileName The filename to associate with the stream content in the `Content-Disposition` HTTP header. - :param: mimeType The MIME type to associate with the stream content in the `Content-Type` HTTP header. + :param: stream The input stream to encode in the multipart form data. + :param: length The content length of the stream. + :param: headers The HTTP headers for the body part. */ - public func appendBodyPart(#stream: NSInputStream, name: String, fileName: String, length: UInt64, mimeType: String) { - let headers = contentHeaders(name: name, fileName: fileName, mimeType: mimeType) + public func appendBodyPart(#stream: NSInputStream, length: UInt64, headers: [String: String]) { let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length) - self.bodyParts.append(bodyPart) } - // MARK: - Data Extraction + // MARK: - Data Encoding /** Encodes all the appended body parts into a single `NSData` object. @@ -299,6 +343,10 @@ public class MultipartFormData { :returns: EncodingResult containing an `NSData` object if the encoding succeeded, an `NSError` otherwise. */ public func encode() -> EncodingResult { + if let bodyPartError = self.bodyPartError { + return .Failure(bodyPartError) + } + var encoded = NSMutableData() self.bodyParts.first?.hasInitialBoundary = true @@ -329,6 +377,11 @@ public class MultipartFormData { :param: completionHandler A closure to be executed when writing is finished. */ public func writeEncodedDataToDisk(fileURL: NSURL, completionHandler: (NSError?) -> Void) { + if let bodyPartError = self.bodyPartError { + completionHandler(bodyPartError) + return + } + var error: NSError? if let path = fileURL.path where NSFileManager.defaultManager().fileExistsAtPath(path) { @@ -598,6 +651,13 @@ public class MultipartFormData { return ["Content-Disposition": "form-data; name=\"\(name)\""] } + private func contentHeaders(#name: String, mimeType: String) -> [String: String] { + return [ + "Content-Disposition": "form-data; name=\"\(name)\"", + "Content-Type": "\(mimeType)" + ] + } + private func contentHeaders(#name: String, fileName: String, mimeType: String) -> [String: String] { return [ "Content-Disposition": "form-data; name=\"\(name)\"; filename=\"\(fileName)\"", @@ -621,6 +681,12 @@ public class MultipartFormData { // MARK: - Private - Errors + private func setBodyPartError(error: NSError) { + if self.bodyPartError == nil { + self.bodyPartError = error + } + } + private func errorWithCode(code: Int, failureReason: String) -> NSError { let userInfo = [NSLocalizedFailureReasonErrorKey: failureReason] return NSError(domain: AlamofireErrorDomain, code: code, userInfo: userInfo) diff --git a/Source/ParameterEncoding.swift b/Source/ParameterEncoding.swift index 6a2394376..8a8695cd2 100644 --- a/Source/ParameterEncoding.swift +++ b/Source/ParameterEncoding.swift @@ -103,8 +103,7 @@ public enum ParameterEncoding { } } - let method = Method(rawValue: mutableURLRequest.HTTPMethod) - if let method = method where encodesParametersInURL(method) { + if let method = Method(rawValue: mutableURLRequest.HTTPMethod) where encodesParametersInURL(method) { if let URLComponents = NSURLComponents(URL: mutableURLRequest.URL!, resolvingAgainstBaseURL: false) { URLComponents.percentEncodedQuery = (URLComponents.percentEncodedQuery.map { $0 + "&" } ?? "") + query(parameters!) mutableURLRequest.URL = URLComponents.URL @@ -118,6 +117,7 @@ public enum ParameterEncoding { } case .JSON: let options = NSJSONWritingOptions.allZeros + if let data = NSJSONSerialization.dataWithJSONObject(parameters!, options: options, error: &error) { mutableURLRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") mutableURLRequest.HTTPBody = data diff --git a/Source/Request.swift b/Source/Request.swift index 95677cb18..f4b41d0dd 100644 --- a/Source/Request.swift +++ b/Source/Request.swift @@ -135,56 +135,6 @@ public class Request { return self } - // MARK: - Response - - /** - A closure used by response handlers that takes a request, response, and data and returns a serialized object and any error that occured in the process. - */ - public typealias Serializer = (NSURLRequest, NSHTTPURLResponse?, NSData?) -> (AnyObject?, NSError?) - - /** - Creates a response serializer that returns the associated data as-is. - - :returns: A data response serializer. - */ - public class func responseDataSerializer() -> Serializer { - return { request, response, data in - return (data, nil) - } - } - - /** - Adds a handler to be called once the request has finished. - - :param: completionHandler The code to be executed once the request has finished. - - :returns: The request. - */ - public func response(completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) -> Self { - return response(serializer: Request.responseDataSerializer(), completionHandler: completionHandler) - } - - /** - Adds a handler to be called once the request has finished. - - :param: queue The queue on which the completion handler is dispatched. - :param: serializer The closure responsible for serializing the request, response, and data. - :param: completionHandler The code to be executed once the request has finished. - - :returns: The request. - */ - public func response(queue: dispatch_queue_t? = nil, serializer: Serializer, completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) -> Self { - self.delegate.queue.addOperationWithBlock { - let (responseObject: AnyObject?, serializationError: NSError?) = serializer(self.request, self.response, self.delegate.data) - - dispatch_async(queue ?? dispatch_get_main_queue()) { - completionHandler(self.request, self.response, responseObject, self.delegate.error ?? serializationError) - } - } - - return self - } - // MARK: - State /** @@ -284,6 +234,20 @@ public class Request { if let taskDidReceiveChallenge = self.taskDidReceiveChallenge { (disposition, credential) = taskDidReceiveChallenge(session, task, challenge) + } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + let host = challenge.protectionSpace.host + + if let + serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicyForHost(host), + serverTrust = challenge.protectionSpace.serverTrust + { + if serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) { + disposition = .UseCredential + credential = NSURLCredential(forTrust: serverTrust) + } else { + disposition = .CancelAuthenticationChallenge + } + } } else { if challenge.previousFailureCount > 0 { disposition = .CancelAuthenticationChallenge @@ -313,8 +277,16 @@ public class Request { if let taskDidCompleteWithError = self.taskDidCompleteWithError { taskDidCompleteWithError(session, task, error) } else { - if error != nil { + if let error = error { self.error = error + + if let + downloadDelegate = self as? DownloadTaskDelegate, + userInfo = error.userInfo as? [String: AnyObject], + resumeData = userInfo[NSURLSessionDownloadTaskResumeData] as? NSData + { + downloadDelegate.resumeData = resumeData + } } self.queue.suspended = false @@ -411,6 +383,7 @@ extension Request: Printable { /// The textual representation used when written to an output stream, which includes the HTTP method and URL, as well as the response status code if a response has been received. public var description: String { var components: [String] = [] + if let HTTPMethod = self.request.HTTPMethod { components.append(HTTPMethod) } @@ -438,7 +411,14 @@ extension Request: DebugPrintable { } if let credentialStorage = self.session.configuration.URLCredentialStorage { - let protectionSpace = NSURLProtectionSpace(host: URL!.host!, port: URL!.port?.integerValue ?? 0, `protocol`: URL!.scheme!, realm: URL!.host!, authenticationMethod: NSURLAuthenticationMethodHTTPBasic) + let protectionSpace = NSURLProtectionSpace( + host: URL!.host!, + port: URL!.port?.integerValue ?? 0, + `protocol`: URL!.scheme!, + realm: URL!.host!, + authenticationMethod: NSURLAuthenticationMethodHTTPBasic + ) + if let credentials = credentialStorage.credentialsForProtectionSpace(protectionSpace)?.values.array { for credential: NSURLCredential in (credentials as! [NSURLCredential]) { components.append("-u \(credential.user!):\(credential.password!)") @@ -454,9 +434,9 @@ extension Request: DebugPrintable { // See https://github.com/CocoaPods/swift/issues/24 #if !os(OSX) if self.session.configuration.HTTPShouldSetCookies { - if let cookieStorage = self.session.configuration.HTTPCookieStorage, - cookies = cookieStorage.cookiesForURL(URL!) as? [NSHTTPCookie] - where !cookies.isEmpty + if let + cookieStorage = self.session.configuration.HTTPCookieStorage, + cookies = cookieStorage.cookiesForURL(URL!) as? [NSHTTPCookie] where !cookies.isEmpty { let string = cookies.reduce(""){ $0 + "\($1.name)=\($1.value ?? String());" } components.append("-b \"\(string.substringToIndex(string.endIndex.predecessor()))\"") diff --git a/Source/ResponseSerialization.swift b/Source/ResponseSerialization.swift index 3766dfae0..4ae703cad 100644 --- a/Source/ResponseSerialization.swift +++ b/Source/ResponseSerialization.swift @@ -22,9 +22,95 @@ import Foundation -// MARK: String +// MARK: - ResponseSerializer + +/** + The type in which all response serializers must conform to in order to serialize a response. +*/ +public protocol ResponseSerializer { + /// The type of serialized object to be created by this `ResponseSerializer`. + typealias SerializedObject + + /// A closure used by response handlers that takes a request, response, and data and returns a serialized object and any error that occured in the process. + var serializeResponse: (NSURLRequest?, NSHTTPURLResponse?, NSData?) -> (SerializedObject?, NSError?) { get } +} + +/** + A generic `ResponseSerializer` used to serialize a request, response, and data into a serialized object. +*/ +public struct GenericResponseSerializer: ResponseSerializer { + /// The type of serialized object to be created by this `ResponseSerializer`. + public typealias SerializedObject = T + + /// A closure used by response handlers that takes a request, response, and data and returns a serialized object and any error that occured in the process. + public var serializeResponse: (NSURLRequest?, NSHTTPURLResponse?, NSData?) -> (SerializedObject?, NSError?) +} + +// MARK: - Default extension Request { + + /** + Adds a handler to be called once the request has finished. + + :param: queue The queue on which the completion handler is dispatched. + :param: responseSerializer The response serializer responsible for serializing the request, response, and data. + :param: completionHandler The code to be executed once the request has finished. + + :returns: The request. + */ + public func response( + queue: dispatch_queue_t? = nil, + responseSerializer: T, + completionHandler: (NSURLRequest, NSHTTPURLResponse?, V?, NSError?) -> Void) + -> Self + { + self.delegate.queue.addOperationWithBlock { + let result: V? + let error: NSError? + + (result, error) = responseSerializer.serializeResponse(self.request, self.response, self.delegate.data) + + dispatch_async(queue ?? dispatch_get_main_queue()) { + completionHandler(self.request, self.response, result, self.delegate.error ?? error) + } + } + + return self + } +} + +// MARK: - Data + +extension Request { + + /** + Creates a response serializer that returns the associated data as-is. + + :returns: A data response serializer. + */ + public static func dataResponseSerializer() -> GenericResponseSerializer { + return GenericResponseSerializer { request, response, data in + return (data, nil) + } + } + + /** + Adds a handler to be called once the request has finished. + + :param: completionHandler The code to be executed once the request has finished. + + :returns: The request. + */ + public func response(completionHandler: (NSURLRequest, NSHTTPURLResponse?, NSData?, NSError?) -> Void) -> Self { + return response(responseSerializer: Request.dataResponseSerializer(), completionHandler: completionHandler) + } +} + +// MARK: - String + +extension Request { + /** Creates a response serializer that returns a string initialized from the response data with the specified string encoding. @@ -32,19 +118,17 @@ extension Request { :returns: A string response serializer. */ - public class func stringResponseSerializer(var encoding: NSStringEncoding? = nil) -> Serializer { - return { _, response, data in + public static func stringResponseSerializer(var encoding: NSStringEncoding? = nil) -> GenericResponseSerializer { + return GenericResponseSerializer { _, response, data in if data == nil || data?.length == 0 { return (nil, nil) } - if encoding == nil { - if let encodingName = response?.textEncodingName { - encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding(encodingName)) - } + if let encodingName = response?.textEncodingName where encoding == nil { + encoding = CFStringConvertEncodingToNSStringEncoding(CFStringConvertIANACharSetNameToEncoding(encodingName)) } - let string = NSString(data: data!, encoding: encoding ?? NSISOLatin1StringEncoding) + let string = NSString(data: data!, encoding: encoding ?? NSISOLatin1StringEncoding) as? String return (string, nil) } @@ -58,16 +142,22 @@ extension Request { :returns: The request. */ - public func responseString(encoding: NSStringEncoding? = nil, completionHandler: (NSURLRequest, NSHTTPURLResponse?, String?, NSError?) -> Void) -> Self { - return response(serializer: Request.stringResponseSerializer(encoding: encoding), completionHandler: { request, response, string, error in - completionHandler(request, response, string as? String, error) - }) + public func responseString( + encoding: NSStringEncoding? = nil, + completionHandler: (NSURLRequest, NSHTTPURLResponse?, String?, NSError?) -> Void) + -> Self + { + return response( + responseSerializer: Request.stringResponseSerializer(encoding: encoding), + completionHandler: completionHandler + ) } } // MARK: - JSON extension Request { + /** Creates a response serializer that returns a JSON object constructed from the response data using `NSJSONSerialization` with the specified reading options. @@ -75,8 +165,8 @@ extension Request { :returns: A JSON object response serializer. */ - public class func JSONResponseSerializer(options: NSJSONReadingOptions = .AllowFragments) -> Serializer { - return { request, response, data in + public static func JSONResponseSerializer(options: NSJSONReadingOptions = .AllowFragments) -> GenericResponseSerializer { + return GenericResponseSerializer { request, response, data in if data == nil || data?.length == 0 { return (nil, nil) } @@ -96,16 +186,22 @@ extension Request { :returns: The request. */ - public func responseJSON(options: NSJSONReadingOptions = .AllowFragments, completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) -> Self { - return response(serializer: Request.JSONResponseSerializer(options: options), completionHandler: { request, response, JSON, error in - completionHandler(request, response, JSON, error) - }) + public func responseJSON( + options: NSJSONReadingOptions = .AllowFragments, + completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) + -> Self + { + return response( + responseSerializer: Request.JSONResponseSerializer(options: options), + completionHandler: completionHandler + ) } } // MARK: - Property List extension Request { + /** Creates a response serializer that returns an object constructed from the response data using `NSPropertyListSerialization` with the specified reading options. @@ -113,14 +209,19 @@ extension Request { :returns: A property list object response serializer. */ - public class func propertyListResponseSerializer(options: NSPropertyListReadOptions = 0) -> Serializer { - return { request, response, data in + public static func propertyListResponseSerializer(options: NSPropertyListReadOptions = 0) -> GenericResponseSerializer { + return GenericResponseSerializer { request, response, data in if data == nil || data?.length == 0 { return (nil, nil) } var propertyListSerializationError: NSError? - let plist: AnyObject? = NSPropertyListSerialization.propertyListWithData(data!, options: options, format: nil, error: &propertyListSerializationError) + let plist: AnyObject? = NSPropertyListSerialization.propertyListWithData( + data!, + options: options, + format: nil, + error: &propertyListSerializationError + ) return (plist, propertyListSerializationError) } @@ -134,9 +235,14 @@ extension Request { :returns: The request. */ - public func responsePropertyList(options: NSPropertyListReadOptions = 0, completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) -> Self { - return response(serializer: Request.propertyListResponseSerializer(options: options), completionHandler: { request, response, plist, error in - completionHandler(request, response, plist, error) - }) + public func responsePropertyList( + options: NSPropertyListReadOptions = 0, + completionHandler: (NSURLRequest, NSHTTPURLResponse?, AnyObject?, NSError?) -> Void) + -> Self + { + return response( + responseSerializer: Request.propertyListResponseSerializer(options: options), + completionHandler: completionHandler + ) } } diff --git a/Source/ServerTrustPolicy.swift b/Source/ServerTrustPolicy.swift new file mode 100644 index 000000000..9e3f551a1 --- /dev/null +++ b/Source/ServerTrustPolicy.swift @@ -0,0 +1,286 @@ +// ServerTrustPolicy.swift +// +// Copyright (c) 2014–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Responsible for managing the mapping of `ServerTrustPolicy` objects to a given host. +public class ServerTrustPolicyManager { + let policies: [String: ServerTrustPolicy] + + /** + Initializes the `ServerTrustPolicyManager` instance with the given policies. + + Since different servers and web services can have different leaf certificates, intermediate and even root + certficates, it is important to have the flexibility to specify evaluation policies on a per host basis. This + allows for scenarios such as using default evaluation for host1, certificate pinning for host2, public key + pinning for host3 and disabling evaluation for host4. + + :param: policies A dictionary of all policies mapped to a particular host. + + :returns: The new `ServerTrustPolicyManager` instance. + */ + public init(policies: [String: ServerTrustPolicy]) { + self.policies = policies + } + + func serverTrustPolicyForHost(host: String) -> ServerTrustPolicy? { + return self.policies[host] + } +} + +// MARK: - + +extension NSURLSession { + private struct AssociatedKeys { + static var ManagerKey = "NSURLSession.ServerTrustPolicyManager" + } + + var serverTrustPolicyManager: ServerTrustPolicyManager? { + get { + return objc_getAssociatedObject(self, &AssociatedKeys.ManagerKey) as? ServerTrustPolicyManager + } + set (manager) { + objc_setAssociatedObject(self, &AssociatedKeys.ManagerKey, manager, UInt(OBJC_ASSOCIATION_RETAIN_NONATOMIC)) + } + } +} + +// MARK: - ServerTrustPolicy + +/** + The `ServerTrustPolicy` evaluates the server trust generally provided by an `NSURLAuthenticationChallenge` when + connecting to a server over a secure HTTPS connection. The policy configuration then evaluates the server trust + with a given set of criteria to determine whether the server trust is valid and the connection should be made. + + Using pinned certificates or public keys for evaluation helps prevent man-in-the-middle (MITM) attacks and other + vulnerabilities. Applications dealing with sensitive customer data or financial information are strongly encouraged + to route all communication over an HTTPS connection with pinning enabled. + + - PerformDefaultEvaluation: Uses the default server trust evaluation while allowing you to control whether to + validate the host provided by the challenge. Applications are encouraged to always + validate the host in production environments to guarantee the validity of the server's + certificate chain. + + - PinCertificates: Uses the pinned certificates to validate the server trust. The server trust is + considered valid if one of the pinned certificates match one of the server certificates. + By validating both the certificate chain and host, certificate pinning provides a very + secure form of server trust validation mitigating most, if not all, MITM attacks. + Applications are encouraged to always validate the host and require a valid certificate + chain in production environments. + + - PinPublicKeys: Uses the pinned public keys to validate the server trust. The server trust is considered + valid if one of the pinned public keys match one of the server certificate public keys. + By validating both the certificate chain and host, public key pinning provides a very + secure form of server trust validation mitigating most, if not all, MITM attacks. + Applications are encouraged to always validate the host and require a valid certificate + chain in production environments. + + - DisableEvaluation: Disables all evaluation which in turn will always consider any server trust as valid. + + - CustomEvaluation: Uses the associated closure to evaluate the validity of the server trust. +*/ +public enum ServerTrustPolicy { + case PerformDefaultEvaluation(validateHost: Bool) + case PinCertificates(certificates: [SecCertificate], validateCertificateChain: Bool, validateHost: Bool) + case PinPublicKeys(publicKeys: [SecKey], validateCertificateChain: Bool, validateHost: Bool) + case DisableEvaluation + case CustomEvaluation((serverTrust: SecTrust, host: String) -> Bool) + + // MARK: - Bundle Location + + /** + Returns all certificates within the given bundle with a `.cer` file extension. + + :param: bundle The bundle to search for all `.cer` files. + + :returns: All certificates within the given bundle. + */ + public static func certificatesInBundle(bundle: NSBundle = NSBundle.mainBundle()) -> [SecCertificate] { + var certificates: [SecCertificate] = [] + + for path in bundle.pathsForResourcesOfType(".cer", inDirectory: nil) as! [String] { + if let + certificateData = NSData(contentsOfFile: path), + certificate = SecCertificateCreateWithData(nil, certificateData)?.takeRetainedValue() + { + certificates.append(certificate) + } + } + + return certificates + } + + /** + Returns all public keys within the given bundle with a `.cer` file extension. + + :param: bundle The bundle to search for all `*.cer` files. + + :returns: All public keys within the given bundle. + */ + public static func publicKeysInBundle(bundle: NSBundle = NSBundle.mainBundle()) -> [SecKey] { + var publicKeys: [SecKey] = [] + + for certificate in certificatesInBundle(bundle: bundle) { + if let publicKey = publicKeyForCertificate(certificate) { + publicKeys.append(publicKey) + } + } + + return publicKeys + } + + // MARK: - Evaluation + + /** + Evaluates whether the server trust is valid for the given host. + + :param: serverTrust The server trust to evaluate. + :param: host The host of the challenge protection space. + + :returns: Whether the server trust is valid. + */ + public func evaluateServerTrust(serverTrust: SecTrust, isValidForHost host: String) -> Bool { + var serverTrustIsValid = false + + switch self { + case let .PerformDefaultEvaluation(validateHost): + let policy = validateHost ? SecPolicyCreateSSL(1, host as CFString) : SecPolicyCreateBasicX509() + SecTrustSetPolicies(serverTrust, [policy.takeRetainedValue()]) + + serverTrustIsValid = trustIsValid(serverTrust) + case let .PinCertificates(pinnedCertificates, validateCertificateChain, validateHost): + if validateCertificateChain { + let policy = validateHost ? SecPolicyCreateSSL(1, host as CFString) : SecPolicyCreateBasicX509() + SecTrustSetPolicies(serverTrust, [policy.takeRetainedValue()]) + + SecTrustSetAnchorCertificates(serverTrust, pinnedCertificates) + SecTrustSetAnchorCertificatesOnly(serverTrust, 1) + + serverTrustIsValid = trustIsValid(serverTrust) + } else { + let serverCertificatesDataArray = certificateDataForTrust(serverTrust) + let pinnedCertificatesDataArray = certificateDataForCertificates(pinnedCertificates) + + outerLoop: for serverCertificateData in serverCertificatesDataArray { + for pinnedCertificateData in pinnedCertificatesDataArray { + if serverCertificateData.isEqualToData(pinnedCertificateData) { + serverTrustIsValid = true + break outerLoop + } + } + } + } + case let .PinPublicKeys(pinnedPublicKeys, validateCertificateChain, validateHost): + var certificateChainEvaluationPassed = true + + if validateCertificateChain { + let policy = validateHost ? SecPolicyCreateSSL(1, host as CFString) : SecPolicyCreateBasicX509() + SecTrustSetPolicies(serverTrust, [policy.takeRetainedValue()]) + + certificateChainEvaluationPassed = trustIsValid(serverTrust) + } + + if certificateChainEvaluationPassed { + let serverKeys = ServerTrustPolicy.publicKeysForTrust(serverTrust) + outerLoop: for serverPublicKey in ServerTrustPolicy.publicKeysForTrust(serverTrust) as [AnyObject] { + for pinnedPublicKey in pinnedPublicKeys as [AnyObject] { + if serverPublicKey.isEqual(pinnedPublicKey) { + serverTrustIsValid = true + break outerLoop + } + } + } + } + case .DisableEvaluation: + serverTrustIsValid = true + case let .CustomEvaluation(closure): + serverTrustIsValid = closure(serverTrust: serverTrust, host: host) + } + + return serverTrustIsValid + } + + // MARK: - Private - Trust Validation + + private func trustIsValid(trust: SecTrust) -> Bool { + var isValid = false + + var result = SecTrustResultType(kSecTrustResultInvalid) + let status = SecTrustEvaluate(trust, &result) + + if status == errSecSuccess { + let unspecified = SecTrustResultType(kSecTrustResultUnspecified) + let proceed = SecTrustResultType(kSecTrustResultProceed) + + isValid = result == unspecified || result == proceed + } + + return isValid + } + + // MARK: - Private - Certificate Data + + private func certificateDataForTrust(trust: SecTrust) -> [NSData] { + var certificates: [SecCertificate] = [] + + for index in 0.. [NSData] { + return certificates.map { SecCertificateCopyData($0).takeRetainedValue() as NSData } + } + + // MARK: - Private - Public Key Extraction + + private static func publicKeysForTrust(trust: SecTrust) -> [SecKey] { + var publicKeys: [SecKey] = [] + + for index in 0.. SecKey? { + var publicKey: SecKey? + + let policy = SecPolicyCreateBasicX509().takeRetainedValue() + var unmanagedTrust: Unmanaged? + let trustCreationStatus = SecTrustCreateWithCertificates(certificate, policy, &unmanagedTrust) + + if let trust = unmanagedTrust?.takeRetainedValue() where trustCreationStatus == errSecSuccess { + publicKey = SecTrustCopyPublicKey(trust).takeRetainedValue() + } + + return publicKey + } +} diff --git a/Source/Upload.swift b/Source/Upload.swift index 90000b08a..ce27da678 100644 --- a/Source/Upload.swift +++ b/Source/Upload.swift @@ -46,15 +46,18 @@ extension Manager { dispatch_sync(self.queue) { uploadTask = self.session.uploadTaskWithStreamedRequest(request) } + HTTPBodyStream = stream } let request = Request(session: self.session, task: uploadTask) + if HTTPBodyStream != nil { request.delegate.taskNeedNewBodyStream = { _, _ in return HTTPBodyStream } } + self.delegate[request.delegate.task] = request.delegate if self.startRequestsImmediately { @@ -127,6 +130,7 @@ extension Manager { */ public func upload(method: Method, _ URLString: URLStringConvertible, headers: [String: String]? = nil, data: NSData) -> Request { let mutableURLRequest = URLRequest(method, URLString, headers: headers) + return upload(mutableURLRequest, data: data) } @@ -160,6 +164,7 @@ extension Manager { */ public func upload(method: Method, _ URLString: URLStringConvertible, headers: [String: String]? = nil, stream: NSInputStream) -> Request { let mutableURLRequest = URLRequest(method, URLString, headers: headers) + return upload(mutableURLRequest, stream: stream) } diff --git a/Source/Validation.swift b/Source/Validation.swift index 8331bef3f..417f5133c 100644 --- a/Source/Validation.swift +++ b/Source/Validation.swift @@ -72,9 +72,12 @@ extension Request { let subtype: String init?(_ string: String) { - let components = string.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()).substringToIndex(string.rangeOfString(";")?.endIndex ?? string.endIndex).componentsSeparatedByString("/") + let components = string.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) + .substringToIndex(string.rangeOfString(";")?.endIndex ?? string.endIndex) + .componentsSeparatedByString("/") - if let type = components.first, + if let + type = components.first, subtype = components.last { self.type = type @@ -110,9 +113,13 @@ extension Request { responseMIMEType = MIMEType(responseContentType) { for contentType in acceptableContentTypes { - if let acceptableMIMEType = MIMEType(contentType) - where acceptableMIMEType.matches(responseMIMEType) - { + if let acceptableMIMEType = MIMEType(contentType) where acceptableMIMEType.matches(responseMIMEType) { + return true + } + } + } else { + for contentType in acceptableContentTypes { + if let MIMEType = MIMEType(contentType) where MIMEType.type == "*" && MIMEType.subtype == "*" { return true } } diff --git a/Tests/AuthenticationTests.swift b/Tests/AuthenticationTests.swift index 01475d08d..80b8bfd94 100644 --- a/Tests/AuthenticationTests.swift +++ b/Tests/AuthenticationTests.swift @@ -59,7 +59,7 @@ class BasicAuthenticationTestCase: AuthenticationTestCase { var request: NSURLRequest? var response: NSHTTPURLResponse? - var data: AnyObject? + var data: NSData? var error: NSError? // When @@ -90,7 +90,7 @@ class BasicAuthenticationTestCase: AuthenticationTestCase { var request: NSURLRequest? var response: NSHTTPURLResponse? - var data: AnyObject? + var data: NSData? var error: NSError? // When @@ -132,7 +132,7 @@ class HTTPDigestAuthenticationTestCase: AuthenticationTestCase { var request: NSURLRequest? var response: NSHTTPURLResponse? - var data: AnyObject? + var data: NSData? var error: NSError? // When @@ -163,7 +163,7 @@ class HTTPDigestAuthenticationTestCase: AuthenticationTestCase { var request: NSURLRequest? var response: NSHTTPURLResponse? - var data: AnyObject? + var data: NSData? var error: NSError? // When diff --git a/Tests/CacheTests.swift b/Tests/CacheTests.swift index ecd805baf..af6cdd53f 100644 --- a/Tests/CacheTests.swift +++ b/Tests/CacheTests.swift @@ -182,7 +182,7 @@ class CacheTestCase: BaseTestCase { let request = self.manager.request(urlRequest) request.response( queue: queue, - serializer: Request.responseDataSerializer(), + responseSerializer: Request.dataResponseSerializer(), completionHandler: { _, response, _, _ in completion(request.request, response) } diff --git a/Tests/DownloadTests.swift b/Tests/DownloadTests.swift index ac290a45d..d99c27864 100644 --- a/Tests/DownloadTests.swift +++ b/Tests/DownloadTests.swift @@ -66,13 +66,9 @@ class DownloadInitializationTestCase: BaseTestCase { // MARK: - class DownloadResponseTestCase: BaseTestCase { - // MARK: - Properties - let searchPathDirectory: NSSearchPathDirectory = .CachesDirectory let searchPathDomain: NSSearchPathDomainMask = .UserDomainMask - // MARK: - Tests - func testDownloadRequest() { // Given let numberOfLines = 100 @@ -154,7 +150,7 @@ class DownloadResponseTestCase: BaseTestCase { var progressValues: [(completedUnitCount: Int64, totalUnitCount: Int64)] = [] var responseRequest: NSURLRequest? var responseResponse: NSHTTPURLResponse? - var responseData: AnyObject? + var responseData: NSData? var responseError: NSError? // When @@ -181,7 +177,7 @@ class DownloadResponseTestCase: BaseTestCase { // Then XCTAssertNotNil(responseRequest, "response request should not be nil") - XCTAssertNotNil(responseResponse, "response response should not be nil") + XCTAssertNotNil(responseResponse, "response should not be nil") XCTAssertNil(responseData, "response data should be nil") XCTAssertNil(responseError, "response error should be nil") @@ -198,7 +194,8 @@ class DownloadResponseTestCase: BaseTestCase { } } - if let lastByteValue = byteValues.last, + if let + lastByteValue = byteValues.last, lastProgressValue = progressValues.last { let byteValueFractionalCompletion = Double(lastByteValue.totalBytes) / Double(lastByteValue.totalBytesExpected) @@ -215,3 +212,125 @@ class DownloadResponseTestCase: BaseTestCase { XCTAssertNil(removalError, "removal error should be nil") } } + +// MARK: - + +class DownloadResumeDataTestCase: BaseTestCase { + let URLString = "https://upload.wikimedia.org/wikipedia/commons/6/69/NASA-HS201427a-HubbleUltraDeepField2014-20140603.jpg" + let destination: Request.DownloadFileDestination = { + let searchPathDirectory: NSSearchPathDirectory = .CachesDirectory + let searchPathDomain: NSSearchPathDomainMask = .UserDomainMask + + return Request.suggestedDownloadDestination(directory: searchPathDirectory, domain: searchPathDomain) + }() + + func testThatImmediatelyCancelledDownloadDoesNotHaveResumeDataAvailable() { + // Given + let expectation = expectationWithDescription("Download should be cancelled") + + var request: NSURLRequest? + var response: NSHTTPURLResponse? + var data: AnyObject? + var error: NSError? + + // When + let download = Alamofire.download(.GET, self.URLString, destination: self.destination) + .response { responseRequest, responseResponse, responseData, responseError in + request = responseRequest + response = responseResponse + data = responseData + error = responseError + + expectation.fulfill() + } + + download.cancel() + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNotNil(request, "request should not be nil") + XCTAssertNil(response, "response should be nil") + XCTAssertNil(data, "data should be nil") + XCTAssertNotNil(error, "error should not be nil") + + XCTAssertNil(download.resumeData, "resume data should be nil") + } + + func testThatCancelledDownloadResponseDataMatchesResumeData() { + // Given + let expectation = expectationWithDescription("Download should be cancelled") + + var request: NSURLRequest? + var response: NSHTTPURLResponse? + var data: AnyObject? + var error: NSError? + + // When + let download = Alamofire.download(.GET, self.URLString, destination: self.destination) + download.progress { _, _, _ in + download.cancel() + } + download.response { responseRequest, responseResponse, responseData, responseError in + request = responseRequest + response = responseResponse + data = responseData + error = responseError + + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNotNil(request, "request should not be nil") + XCTAssertNotNil(response, "response should not be nil") + XCTAssertNotNil(data, "data should not be nil") + XCTAssertNotNil(error, "error should not be nil") + + XCTAssertNotNil(download.resumeData, "resume data should not be nil") + + if let + responseData = data as? NSData, + resumeData = download.resumeData + { + XCTAssertEqual(responseData, resumeData, "response data should equal resume data") + } else { + XCTFail("response data or resume data was unexpectedly nil") + } + } + + func testThatCancelledDownloadResumeDataIsAvailableWithJSONResponseSerializer() { + // Given + let expectation = expectationWithDescription("Download should be cancelled") + + var request: NSURLRequest? + var response: NSHTTPURLResponse? + var JSON: AnyObject? + var error: NSError? + + // When + let download = Alamofire.download(.GET, self.URLString, destination: self.destination) + download.progress { _, _, _ in + download.cancel() + } + download.responseJSON { responseRequest, responseResponse, responseJSON, responseError in + request = responseRequest + response = responseResponse + JSON = responseJSON + error = responseError + + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNotNil(request, "request should not be nil") + XCTAssertNotNil(response, "response should not be nil") + XCTAssertNil(JSON, "JSON should be nil") + XCTAssertNotNil(error, "error should not be nil") + + XCTAssertNotNil(download.resumeData, "resume data should not be nil") + } +} diff --git a/Tests/MultipartFormDataTests.swift b/Tests/MultipartFormDataTests.swift index 3da2ec79f..f7f556da6 100644 --- a/Tests/MultipartFormDataTests.swift +++ b/Tests/MultipartFormDataTests.swift @@ -141,8 +141,8 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { // When multipartFormData.appendBodyPart(data: french, name: "french") - multipartFormData.appendBodyPart(data: japanese, name: "japanese") - multipartFormData.appendBodyPart(data: emoji, name: "emoji") + multipartFormData.appendBodyPart(data: japanese, name: "japanese", mimeType: "text/plain") + multipartFormData.appendBodyPart(data: emoji, name: "emoji", mimeType: "text/plain") let encodingResult = multipartFormData.encode() // Then @@ -155,10 +155,12 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { "Content-Disposition: form-data; name=\"french\"\(self.CRLF)\(self.CRLF)" + "franΓ§ais" + BoundaryGenerator.boundary(boundaryType: .Encapsulated, boundaryKey: boundary) + - "Content-Disposition: form-data; name=\"japanese\"\(self.CRLF)\(self.CRLF)" + + "Content-Disposition: form-data; name=\"japanese\"\(self.CRLF)" + + "Content-Type: text/plain\(self.CRLF)\(self.CRLF)" + "ζ—₯本θͺž" + BoundaryGenerator.boundary(boundaryType: .Encapsulated, boundaryKey: boundary) + - "Content-Disposition: form-data; name=\"emoji\"\(self.CRLF)\(self.CRLF)" + + "Content-Disposition: form-data; name=\"emoji\"\(self.CRLF)" + + "Content-Type: text/plain\(self.CRLF)\(self.CRLF)" + "πŸ˜ƒπŸ‘πŸ»πŸ»πŸŽ‰" + BoundaryGenerator.boundary(boundaryType: .Final, boundaryKey: boundary) ).dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)! @@ -248,9 +250,9 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { // When multipartFormData.appendBodyPart( stream: unicornStream, + length: unicornDataLength, name: "unicorn", fileName: "unicorn.png", - length: unicornDataLength, mimeType: "image/png" ) let encodingResult = multipartFormData.encode() @@ -291,16 +293,16 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { // When multipartFormData.appendBodyPart( stream: unicornStream, + length: unicornDataLength, name: "unicorn", fileName: "unicorn.png", - length: unicornDataLength, mimeType: "image/png" ) multipartFormData.appendBodyPart( stream: rainbowStream, + length: rainbowDataLength, name: "rainbow", fileName: "rainbow.jpg", - length: rainbowDataLength, mimeType: "image/jpeg" ) let encodingResult = multipartFormData.encode() @@ -350,9 +352,9 @@ class MultipartFormDataEncodingTestCase: BaseTestCase { multipartFormData.appendBodyPart(fileURL: unicornImageURL, name: "unicorn") multipartFormData.appendBodyPart( stream: rainbowStream, + length: rainbowDataLength, name: "rainbow", fileName: "rainbow.jpg", - length: rainbowDataLength, mimeType: "image/jpeg" ) let encodingResult = multipartFormData.encode() @@ -591,9 +593,9 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { // When multipartFormData.appendBodyPart( stream: unicornStream, + length: unicornDataLength, name: "unicorn", fileName: "unicorn.png", - length: unicornDataLength, mimeType: "image/png" ) @@ -646,16 +648,16 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { // When multipartFormData.appendBodyPart( stream: unicornStream, + length: unicornDataLength, name: "unicorn", fileName: "unicorn.png", - length: unicornDataLength, mimeType: "image/png" ) multipartFormData.appendBodyPart( stream: rainbowStream, + length: rainbowDataLength, name: "rainbow", fileName: "rainbow.jpg", - length: rainbowDataLength, mimeType: "image/jpeg" ) @@ -717,9 +719,9 @@ class MultipartFormDataWriteEncodedDataToDiskTestCase: BaseTestCase { multipartFormData.appendBodyPart(fileURL: unicornImageURL, name: "unicorn") multipartFormData.appendBodyPart( stream: rainbowStream, + length: rainbowDataLength, name: "rainbow", fileName: "rainbow.jpg", - length: rainbowDataLength, mimeType: "image/jpeg" ) @@ -774,8 +776,17 @@ class MultipartFormDataFailureTestCase: BaseTestCase { let fileURL = NSURL(string: "")! let multipartFormData = MultipartFormData() + var error: NSError? + // When - let error = multipartFormData.appendBodyPart(fileURL: fileURL, name: "empty_data") + multipartFormData.appendBodyPart(fileURL: fileURL, name: "empty_data") + + switch multipartFormData.encode() { + case .Failure(let encodingError): + error = encodingError + default: + break + } // Then XCTAssertNotNil(error, "error should not be nil") @@ -798,8 +809,17 @@ class MultipartFormDataFailureTestCase: BaseTestCase { let fileURL = NSURL(string: "http://example.com/image.jpg")! let multipartFormData = MultipartFormData() + var error: NSError? + // When - let error = multipartFormData.appendBodyPart(fileURL: fileURL, name: "empty_data") + multipartFormData.appendBodyPart(fileURL: fileURL, name: "empty_data") + + switch multipartFormData.encode() { + case .Failure(let encodingError): + error = encodingError + default: + break + } // Then XCTAssertNotNil(error, "error should not be nil") @@ -822,8 +842,17 @@ class MultipartFormDataFailureTestCase: BaseTestCase { let fileURL = NSURL(fileURLWithPath: NSTemporaryDirectory().stringByAppendingPathComponent("does_not_exist.jpg"))! let multipartFormData = MultipartFormData() + var error: NSError? + // When - let error = multipartFormData.appendBodyPart(fileURL: fileURL, name: "empty_data") + multipartFormData.appendBodyPart(fileURL: fileURL, name: "empty_data") + + switch multipartFormData.encode() { + case .Failure(let encodingError): + error = encodingError + default: + break + } // Then XCTAssertNotNil(error, "error should not be nil") @@ -846,8 +875,17 @@ class MultipartFormDataFailureTestCase: BaseTestCase { let directoryURL = NSURL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)! let multipartFormData = MultipartFormData() + var error: NSError? + // When - let error = multipartFormData.appendBodyPart(fileURL: directoryURL, name: "empty_data") + multipartFormData.appendBodyPart(fileURL: directoryURL, name: "empty_data") + + switch multipartFormData.encode() { + case .Failure(let encodingError): + error = encodingError + default: + break + } // Then XCTAssertNotNil(error, "error should not be nil") diff --git a/Tests/RequestTests.swift b/Tests/RequestTests.swift index 8c4774055..47e3f2555 100644 --- a/Tests/RequestTests.swift +++ b/Tests/RequestTests.swift @@ -80,25 +80,25 @@ class RequestResponseTestCase: BaseTestCase { func testRequestResponse() { // Given let URLString = "http://httpbin.org/get" - let serializer = Alamofire.Request.stringResponseSerializer(encoding: NSUTF8StringEncoding) + let serializer = Request.stringResponseSerializer(encoding: NSUTF8StringEncoding) let expectation = expectationWithDescription("GET request should succeed: \(URLString)") var request: NSURLRequest? var response: NSHTTPURLResponse? - var string: AnyObject? + var string: String? var error: NSError? // When Alamofire.request(.GET, URLString, parameters: ["foo": "bar"]) - .response(serializer: serializer) { responseRequest, responseResponse, responseString, responseError in + .response(responseSerializer: serializer) { responseRequest, responseResponse, responseString, responseError in request = responseRequest response = responseResponse string = responseString error = responseError expectation.fulfill() - } + } waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) @@ -120,7 +120,7 @@ class RequestResponseTestCase: BaseTestCase { var progressValues: [(completedUnitCount: Int64, totalUnitCount: Int64)] = [] var responseRequest: NSURLRequest? var responseResponse: NSHTTPURLResponse? - var responseData: AnyObject? + var responseData: NSData? var responseError: NSError? // When @@ -162,7 +162,8 @@ class RequestResponseTestCase: BaseTestCase { } } - if let lastByteValue = byteValues.last, + if let + lastByteValue = byteValues.last, lastProgressValue = progressValues.last { let byteValueFractionalCompletion = Double(lastByteValue.totalBytes) / Double(lastByteValue.totalBytesExpected) @@ -188,7 +189,7 @@ class RequestResponseTestCase: BaseTestCase { var responseRequest: NSURLRequest? var responseResponse: NSHTTPURLResponse? - var responseData: AnyObject? + var responseData: NSData? var responseError: NSError? // When @@ -232,7 +233,8 @@ class RequestResponseTestCase: BaseTestCase { } } - if let lastByteValue = byteValues.last, + if let + lastByteValue = byteValues.last, lastProgressValue = progressValues.last { let byteValueFractionalCompletion = Double(lastByteValue.totalBytes) / Double(lastByteValue.totalBytesExpected) @@ -340,17 +342,17 @@ class RequestDescriptionTestCase: BaseTestCase { class RequestDebugDescriptionTestCase: BaseTestCase { // MARK: Properties - let manager: Alamofire.Manager = { - let manager = Alamofire.Manager(configuration: NSURLSessionConfiguration.defaultSessionConfiguration()) + let manager: Manager = { + let manager = Manager(configuration: NSURLSessionConfiguration.defaultSessionConfiguration()) manager.startRequestsImmediately = false return manager }() - let managerDisallowingCookies: Alamofire.Manager = { + let managerDisallowingCookies: Manager = { let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() configuration.HTTPShouldSetCookies = false - let manager = Alamofire.Manager(configuration: configuration) + let manager = Manager(configuration: configuration) manager.startRequestsImmediately = false return manager @@ -412,6 +414,7 @@ class RequestDebugDescriptionTestCase: BaseTestCase { NSHTTPCookieName: "foo", NSHTTPCookieValue: "bar", ] + let cookie = NSHTTPCookie(properties: properties)! manager.session.configuration.HTTPCookieStorage?.setCookie(cookie) diff --git a/Tests/Resources/Certificates/alamofire.org/*.alamofire.org.cer b/Tests/Resources/Certificates/alamofire.org/*.alamofire.org.cer new file mode 100644 index 000000000..959a5d82d Binary files /dev/null and b/Tests/Resources/Certificates/alamofire.org/*.alamofire.org.cer differ diff --git a/Tests/Resources/Certificates/alamofire.org/alamofire-root-ca.cer b/Tests/Resources/Certificates/alamofire.org/alamofire-root-ca.cer new file mode 100644 index 000000000..b5ae74395 Binary files /dev/null and b/Tests/Resources/Certificates/alamofire.org/alamofire-root-ca.cer differ diff --git a/Tests/Resources/Certificates/alamofire.org/alamofire-signing-ca1.cer b/Tests/Resources/Certificates/alamofire.org/alamofire-signing-ca1.cer new file mode 100644 index 000000000..38596c592 Binary files /dev/null and b/Tests/Resources/Certificates/alamofire.org/alamofire-signing-ca1.cer differ diff --git a/Tests/Resources/Certificates/alamofire.org/alamofire-signing-ca2.cer b/Tests/Resources/Certificates/alamofire.org/alamofire-signing-ca2.cer new file mode 100644 index 000000000..edd135c7a Binary files /dev/null and b/Tests/Resources/Certificates/alamofire.org/alamofire-signing-ca2.cer differ diff --git a/Tests/Resources/Certificates/alamofire.org/expired.cer b/Tests/Resources/Certificates/alamofire.org/expired.cer new file mode 100644 index 000000000..af5e48432 Binary files /dev/null and b/Tests/Resources/Certificates/alamofire.org/expired.cer differ diff --git a/Tests/Resources/Certificates/alamofire.org/missing-dns-name-and-uri.cer b/Tests/Resources/Certificates/alamofire.org/missing-dns-name-and-uri.cer new file mode 100644 index 000000000..9e4ef3cde Binary files /dev/null and b/Tests/Resources/Certificates/alamofire.org/missing-dns-name-and-uri.cer differ diff --git a/Tests/Resources/Certificates/alamofire.org/multiple-dns-names.cer b/Tests/Resources/Certificates/alamofire.org/multiple-dns-names.cer new file mode 100644 index 000000000..39828ebbb Binary files /dev/null and b/Tests/Resources/Certificates/alamofire.org/multiple-dns-names.cer differ diff --git a/Tests/Resources/Certificates/alamofire.org/signed-by-ca1.cer b/Tests/Resources/Certificates/alamofire.org/signed-by-ca1.cer new file mode 100644 index 000000000..1acfcfc0d Binary files /dev/null and b/Tests/Resources/Certificates/alamofire.org/signed-by-ca1.cer differ diff --git a/Tests/Resources/Certificates/alamofire.org/signed-by-ca2.cer b/Tests/Resources/Certificates/alamofire.org/signed-by-ca2.cer new file mode 100644 index 000000000..709889db1 Binary files /dev/null and b/Tests/Resources/Certificates/alamofire.org/signed-by-ca2.cer differ diff --git a/Tests/Resources/Certificates/alamofire.org/test.alamofire.org.cer b/Tests/Resources/Certificates/alamofire.org/test.alamofire.org.cer new file mode 100644 index 000000000..01c404b34 Binary files /dev/null and b/Tests/Resources/Certificates/alamofire.org/test.alamofire.org.cer differ diff --git a/Tests/Resources/Certificates/alamofire.org/valid-dns-name.cer b/Tests/Resources/Certificates/alamofire.org/valid-dns-name.cer new file mode 100644 index 000000000..a5da56d2b Binary files /dev/null and b/Tests/Resources/Certificates/alamofire.org/valid-dns-name.cer differ diff --git a/Tests/Resources/Certificates/alamofire.org/valid-uri.cer b/Tests/Resources/Certificates/alamofire.org/valid-uri.cer new file mode 100644 index 000000000..80838d4d7 Binary files /dev/null and b/Tests/Resources/Certificates/alamofire.org/valid-uri.cer differ diff --git a/Tests/Resources/Certificates/disig.sk/intermediate-ca-disig.cer b/Tests/Resources/Certificates/disig.sk/intermediate-ca-disig.cer new file mode 100644 index 000000000..f3e22ca9c Binary files /dev/null and b/Tests/Resources/Certificates/disig.sk/intermediate-ca-disig.cer differ diff --git a/Tests/Resources/Certificates/disig.sk/root-ca-disig.cer b/Tests/Resources/Certificates/disig.sk/root-ca-disig.cer new file mode 100644 index 000000000..ca18d9cb6 Binary files /dev/null and b/Tests/Resources/Certificates/disig.sk/root-ca-disig.cer differ diff --git a/Tests/Resources/Certificates/disig.sk/testssl-expire.disig.sk.cer b/Tests/Resources/Certificates/disig.sk/testssl-expire.disig.sk.cer new file mode 100644 index 000000000..4faeda708 Binary files /dev/null and b/Tests/Resources/Certificates/disig.sk/testssl-expire.disig.sk.cer differ diff --git a/Tests/Resources/rainbow.jpg b/Tests/Resources/Images/rainbow.jpg similarity index 100% rename from Tests/Resources/rainbow.jpg rename to Tests/Resources/Images/rainbow.jpg diff --git a/Tests/Resources/unicorn.png b/Tests/Resources/Images/unicorn.png similarity index 100% rename from Tests/Resources/unicorn.png rename to Tests/Resources/Images/unicorn.png diff --git a/Tests/ResponseTests.swift b/Tests/ResponseTests.swift index 356af6bbf..813769544 100644 --- a/Tests/ResponseTests.swift +++ b/Tests/ResponseTests.swift @@ -110,7 +110,7 @@ class RedirectResponseTestCase: BaseTestCase { var request: NSURLRequest? var response: NSHTTPURLResponse? - var data: AnyObject? + var data: NSData? var error: NSError? // When @@ -145,7 +145,7 @@ class RedirectResponseTestCase: BaseTestCase { var request: NSURLRequest? var response: NSHTTPURLResponse? - var data: AnyObject? + var data: NSData? var error: NSError? // When @@ -185,7 +185,7 @@ class RedirectResponseTestCase: BaseTestCase { var request: NSURLRequest? var response: NSHTTPURLResponse? - var data: AnyObject? + var data: NSData? var error: NSError? // When @@ -225,7 +225,7 @@ class RedirectResponseTestCase: BaseTestCase { var request: NSURLRequest? var response: NSHTTPURLResponse? - var data: AnyObject? + var data: NSData? var error: NSError? // When @@ -267,7 +267,7 @@ class RedirectResponseTestCase: BaseTestCase { var request: NSURLRequest? var response: NSHTTPURLResponse? - var data: AnyObject? + var data: NSData? var error: NSError? // When diff --git a/Tests/ServerTrustPolicyTests.swift b/Tests/ServerTrustPolicyTests.swift new file mode 100644 index 000000000..af59d3a4a --- /dev/null +++ b/Tests/ServerTrustPolicyTests.swift @@ -0,0 +1,1384 @@ +// MultipartFormDataTests.swift +// +// Copyright (c) 2014–2015 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Alamofire +import Foundation +import XCTest + +private struct TestCertificates { + // Root Certificates + static let RootCA = TestCertificates.certificateWithFileName("alamofire-root-ca") + + // Intermediate Certificates + static let IntermediateCA1 = TestCertificates.certificateWithFileName("alamofire-signing-ca1") + static let IntermediateCA2 = TestCertificates.certificateWithFileName("alamofire-signing-ca2") + + // Leaf Certificates - Signed by CA1 + static let LeafWildcard = TestCertificates.certificateWithFileName("*.alamofire.org") + static let LeafMultipleDNSNames = TestCertificates.certificateWithFileName("multiple-dns-names") + static let LeafSignedByCA1 = TestCertificates.certificateWithFileName("signed-by-ca1") + static let LeafDNSNameAndURI = TestCertificates.certificateWithFileName("test.alamofire.org") + + // Leaf Certificates - Signed by CA2 + static let LeafExpired = TestCertificates.certificateWithFileName("expired") + static let LeafMissingDNSNameAndURI = TestCertificates.certificateWithFileName("missing-dns-name-and-uri") + static let LeafSignedByCA2 = TestCertificates.certificateWithFileName("signed-by-ca2") + static let LeafValidDNSName = TestCertificates.certificateWithFileName("valid-dns-name") + static let LeafValidURI = TestCertificates.certificateWithFileName("valid-uri") + + static func certificateWithFileName(fileName: String) -> SecCertificate { + class Bundle {} + let filePath = NSBundle(forClass: Bundle.self).pathForResource(fileName, ofType: "cer")! + let data = NSData(contentsOfFile: filePath)! + let certificate = SecCertificateCreateWithData(nil, data).takeRetainedValue() + + return certificate + } +} + +// MARK: - + +private struct TestPublicKeys { + // Root Public Keys + static let RootCA = TestPublicKeys.publicKeyForCertificate(TestCertificates.RootCA) + + // Intermediate Public Keys + static let IntermediateCA1 = TestPublicKeys.publicKeyForCertificate(TestCertificates.IntermediateCA1) + static let IntermediateCA2 = TestPublicKeys.publicKeyForCertificate(TestCertificates.IntermediateCA2) + + // Leaf Public Keys - Signed by CA1 + static let LeafWildcard = TestPublicKeys.publicKeyForCertificate(TestCertificates.LeafWildcard) + static let LeafMultipleDNSNames = TestPublicKeys.publicKeyForCertificate(TestCertificates.LeafMultipleDNSNames) + static let LeafSignedByCA1 = TestPublicKeys.publicKeyForCertificate(TestCertificates.LeafSignedByCA1) + static let LeafDNSNameAndURI = TestPublicKeys.publicKeyForCertificate(TestCertificates.LeafDNSNameAndURI) + + // Leaf Public Keys - Signed by CA2 + static let LeafExpired = TestPublicKeys.publicKeyForCertificate(TestCertificates.LeafExpired) + static let LeafMissingDNSNameAndURI = TestPublicKeys.publicKeyForCertificate(TestCertificates.LeafMissingDNSNameAndURI) + static let LeafSignedByCA2 = TestPublicKeys.publicKeyForCertificate(TestCertificates.LeafSignedByCA2) + static let LeafValidDNSName = TestPublicKeys.publicKeyForCertificate(TestCertificates.LeafValidDNSName) + static let LeafValidURI = TestPublicKeys.publicKeyForCertificate(TestCertificates.LeafValidURI) + + static func publicKeyForCertificate(certificate: SecCertificate) -> SecKey { + let policy = SecPolicyCreateBasicX509().takeRetainedValue() + var unmanagedTrust: Unmanaged? + let trustCreationStatus = SecTrustCreateWithCertificates(certificate, policy, &unmanagedTrust) + + let trust = unmanagedTrust!.takeRetainedValue() + let publicKey = SecTrustCopyPublicKey(trust).takeRetainedValue() + + return publicKey + } +} + +// MARK: - + +private enum TestTrusts { + // Leaf Trusts - Signed by CA1 + case LeafWildcard + case LeafMultipleDNSNames + case LeafSignedByCA1 + case LeafDNSNameAndURI + + // Leaf Trusts - Signed by CA2 + case LeafExpired + case LeafMissingDNSNameAndURI + case LeafSignedByCA2 + case LeafValidDNSName + case LeafValidURI + + // Invalid Trusts + case LeafValidDNSNameMissingIntermediate + case LeafValidDNSNameWithIncorrectIntermediate + + var trust: SecTrust { + let trust: SecTrust + + switch self { + case .LeafWildcard: + trust = TestTrusts.trustWithCertificates([ + TestCertificates.LeafWildcard, + TestCertificates.IntermediateCA1, + TestCertificates.RootCA + ]) + case .LeafMultipleDNSNames: + trust = TestTrusts.trustWithCertificates([ + TestCertificates.LeafMultipleDNSNames, + TestCertificates.IntermediateCA1, + TestCertificates.RootCA + ]) + case .LeafSignedByCA1: + trust = TestTrusts.trustWithCertificates([ + TestCertificates.LeafSignedByCA1, + TestCertificates.IntermediateCA1, + TestCertificates.RootCA + ]) + case .LeafDNSNameAndURI: + trust = TestTrusts.trustWithCertificates([ + TestCertificates.LeafDNSNameAndURI, + TestCertificates.IntermediateCA1, + TestCertificates.RootCA + ]) + case .LeafExpired: + trust = TestTrusts.trustWithCertificates([ + TestCertificates.LeafExpired, + TestCertificates.IntermediateCA2, + TestCertificates.RootCA + ]) + case .LeafMissingDNSNameAndURI: + trust = TestTrusts.trustWithCertificates([ + TestCertificates.LeafMissingDNSNameAndURI, + TestCertificates.IntermediateCA2, + TestCertificates.RootCA + ]) + case .LeafSignedByCA2: + trust = TestTrusts.trustWithCertificates([ + TestCertificates.LeafSignedByCA2, + TestCertificates.IntermediateCA2, + TestCertificates.RootCA + ]) + case .LeafValidDNSName: + trust = TestTrusts.trustWithCertificates([ + TestCertificates.LeafValidDNSName, + TestCertificates.IntermediateCA2, + TestCertificates.RootCA + ]) + case .LeafValidURI: + trust = TestTrusts.trustWithCertificates([ + TestCertificates.LeafValidURI, + TestCertificates.IntermediateCA2, + TestCertificates.RootCA + ]) + case LeafValidDNSNameMissingIntermediate: + trust = TestTrusts.trustWithCertificates([ + TestCertificates.LeafValidDNSName, + TestCertificates.RootCA + ]) + case LeafValidDNSNameWithIncorrectIntermediate: + trust = TestTrusts.trustWithCertificates([ + TestCertificates.LeafValidDNSName, + TestCertificates.IntermediateCA1, + TestCertificates.RootCA + ]) + } + + return trust + } + + static func trustWithCertificates(certificates: [SecCertificate]) -> SecTrust { + let policy = SecPolicyCreateBasicX509().takeRetainedValue() + var unmanagedTrust: Unmanaged? + SecTrustCreateWithCertificates(certificates, policy, &unmanagedTrust) + let trust = unmanagedTrust!.takeRetainedValue() + + return trust + } +} + +// MARK: - Basic X509 and SSL Exploration Tests - + +class ServerTrustPolicyTestCase: BaseTestCase { + func setRootCertificateAsLoneAnchorCertificateForTrust(trust: SecTrust) { + SecTrustSetAnchorCertificates(trust, [TestCertificates.RootCA]) + SecTrustSetAnchorCertificatesOnly(trust, 1) + } + + func trustIsValid(trust: SecTrust) -> Bool { + var isValid = false + + var result = SecTrustResultType(kSecTrustResultInvalid) + let status = SecTrustEvaluate(trust, &result) + + if status == errSecSuccess { + let unspecified = SecTrustResultType(kSecTrustResultUnspecified) + let proceed = SecTrustResultType(kSecTrustResultProceed) + + isValid = result == unspecified || result == proceed + } + + return isValid + } +} + +// MARK: - + +class ServerTrustPolicyExplorationBasicX509PolicyValidationTestCase: ServerTrustPolicyTestCase { + func testThatAnchoredRootCertificatePassesBasicX509ValidationWithRootInTrust() { + // Given + let trust = TestTrusts.trustWithCertificates([ + TestCertificates.LeafDNSNameAndURI, + TestCertificates.IntermediateCA1, + TestCertificates.RootCA + ]) + + setRootCertificateAsLoneAnchorCertificateForTrust(trust) + + // When + let policies = [SecPolicyCreateBasicX509().takeRetainedValue()] + SecTrustSetPolicies(trust, policies) + + // Then + XCTAssertTrue(trustIsValid(trust), "trust should be valid") + } + + func testThatAnchoredRootCertificatePassesBasicX509ValidationWithoutRootInTrust() { + // Given + let trust = TestTrusts.LeafDNSNameAndURI.trust + setRootCertificateAsLoneAnchorCertificateForTrust(trust) + + // When + let policies = [SecPolicyCreateBasicX509().takeRetainedValue()] + SecTrustSetPolicies(trust, policies) + + // Then + XCTAssertTrue(trustIsValid(trust), "trust should be valid") + } + + func testThatCertificateMissingDNSNamePassesBasicX509Validation() { + // Given + let trust = TestTrusts.LeafMissingDNSNameAndURI.trust + setRootCertificateAsLoneAnchorCertificateForTrust(trust) + + // When + let policies = [SecPolicyCreateBasicX509().takeRetainedValue()] + SecTrustSetPolicies(trust, policies) + + // Then + XCTAssertTrue(trustIsValid(trust), "trust should be valid") + } + + func testThatExpiredCertificateFailsBasicX509Validation() { + // Given + let trust = TestTrusts.LeafExpired.trust + setRootCertificateAsLoneAnchorCertificateForTrust(trust) + + // When + let policies = [SecPolicyCreateBasicX509().takeRetainedValue()] + SecTrustSetPolicies(trust, policies) + + // Then + XCTAssertFalse(trustIsValid(trust), "trust should not be valid") + } +} + +// MARK: - + +class ServerTrustPolicyExplorationSSLPolicyValidationTestCase: ServerTrustPolicyTestCase { + func testThatAnchoredRootCertificatePassesSSLValidationWithRootInTrust() { + // Given + let trust = TestTrusts.trustWithCertificates([ + TestCertificates.LeafDNSNameAndURI, + TestCertificates.IntermediateCA1, + TestCertificates.RootCA + ]) + + setRootCertificateAsLoneAnchorCertificateForTrust(trust) + + // When + let policies = [SecPolicyCreateSSL(1, "test.alamofire.org").takeRetainedValue()] + SecTrustSetPolicies(trust, policies) + + // Then + XCTAssertTrue(trustIsValid(trust), "trust should be valid") + } + + func testThatAnchoredRootCertificatePassesSSLValidationWithoutRootInTrust() { + // Given + let trust = TestTrusts.LeafDNSNameAndURI.trust + setRootCertificateAsLoneAnchorCertificateForTrust(trust) + + // When + let policies = [SecPolicyCreateSSL(1, "test.alamofire.org").takeRetainedValue()] + SecTrustSetPolicies(trust, policies) + + // Then + XCTAssertTrue(trustIsValid(trust), "trust should be valid") + } + + func testThatCertificateMissingDNSNameFailsSSLValidation() { + // Given + let trust = TestTrusts.LeafMissingDNSNameAndURI.trust + setRootCertificateAsLoneAnchorCertificateForTrust(trust) + + // When + let policies = [SecPolicyCreateSSL(1, "test.alamofire.org").takeRetainedValue()] + SecTrustSetPolicies(trust, policies) + + // Then + XCTAssertFalse(trustIsValid(trust), "trust should not be valid") + } + + func testThatWildcardCertificatePassesSSLValidation() { + // Given + let trust = TestTrusts.LeafWildcard.trust // *.alamofire.org + setRootCertificateAsLoneAnchorCertificateForTrust(trust) + + // When + let policies = [SecPolicyCreateSSL(1, "test.alamofire.org").takeRetainedValue()] + SecTrustSetPolicies(trust, policies) + + // Then + XCTAssertTrue(trustIsValid(trust), "trust should be valid") + } + + func testThatDNSNameCertificatePassesSSLValidation() { + // Given + let trust = TestTrusts.LeafValidDNSName.trust + setRootCertificateAsLoneAnchorCertificateForTrust(trust) + + // When + let policies = [SecPolicyCreateSSL(1, "test.alamofire.org").takeRetainedValue()] + SecTrustSetPolicies(trust, policies) + + // Then + XCTAssertTrue(trustIsValid(trust), "trust should be valid") + } + + func testThatURICertificateFailsSSLValidation() { + // Given + let trust = TestTrusts.LeafValidURI.trust + setRootCertificateAsLoneAnchorCertificateForTrust(trust) + + // When + let policies = [SecPolicyCreateSSL(1, "test.alamofire.org").takeRetainedValue()] + SecTrustSetPolicies(trust, policies) + + // Then + XCTAssertFalse(trustIsValid(trust), "trust should not be valid") + } + + func testThatMultipleDNSNamesCertificatePassesSSLValidationForAllEntries() { + // Given + let trust = TestTrusts.LeafMultipleDNSNames.trust + setRootCertificateAsLoneAnchorCertificateForTrust(trust) + + // When + let policies = [ + SecPolicyCreateSSL(1, "test.alamofire.org").takeRetainedValue(), + SecPolicyCreateSSL(1, "blog.alamofire.org").takeRetainedValue(), + SecPolicyCreateSSL(1, "www.alamofire.org").takeRetainedValue() + ] + SecTrustSetPolicies(trust, policies) + + // Then + XCTAssertTrue(trustIsValid(trust), "trust should not be valid") + } + + func testThatPassingNilForHostParameterAllowsCertificateMissingDNSNameToPassSSLValidation() { + // Given + let trust = TestTrusts.LeafMissingDNSNameAndURI.trust + setRootCertificateAsLoneAnchorCertificateForTrust(trust) + + // When + let policies = [SecPolicyCreateSSL(1, nil).takeRetainedValue()] + SecTrustSetPolicies(trust, policies) + + // Then + XCTAssertTrue(trustIsValid(trust), "trust should not be valid") + } + + func testThatExpiredCertificateFailsSSLValidation() { + // Given + let trust = TestTrusts.LeafExpired.trust + setRootCertificateAsLoneAnchorCertificateForTrust(trust) + + // When + let policies = [SecPolicyCreateSSL(1, "test.alamofire.org").takeRetainedValue()] + SecTrustSetPolicies(trust, policies) + + // Then + XCTAssertFalse(trustIsValid(trust), "trust should not be valid") + } +} + +// MARK: - Server Trust Policy Tests - + +class ServerTrustPolicyPerformDefaultEvaluationTestCase: ServerTrustPolicyTestCase { + + // MARK: Do NOT Validate Host + + func testThatValidCertificateChainPassesEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let serverTrustPolicy = ServerTrustPolicy.PerformDefaultEvaluation(validateHost: false) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatNonAnchoredRootCertificateChainFailsEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let serverTrustPolicy = ServerTrustPolicy.PerformDefaultEvaluation(validateHost: false) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatMissingDNSNameLeafCertificatePassesEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafMissingDNSNameAndURI.trust + let serverTrustPolicy = ServerTrustPolicy.PerformDefaultEvaluation(validateHost: false) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatExpiredCertificateChainFailsEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafExpired.trust + let serverTrustPolicy = ServerTrustPolicy.PerformDefaultEvaluation(validateHost: false) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatMissingIntermediateCertificateInChainFailsEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSNameMissingIntermediate.trust + let serverTrustPolicy = ServerTrustPolicy.PerformDefaultEvaluation(validateHost: false) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + // MARK: Validate Host + + func testThatValidCertificateChainPassesEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let serverTrustPolicy = ServerTrustPolicy.PerformDefaultEvaluation(validateHost: true) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatNonAnchoredRootCertificateChainFailsEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let serverTrustPolicy = ServerTrustPolicy.PerformDefaultEvaluation(validateHost: true) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatMissingDNSNameLeafCertificateFailsEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafMissingDNSNameAndURI.trust + let serverTrustPolicy = ServerTrustPolicy.PerformDefaultEvaluation(validateHost: true) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatWildcardedLeafCertificateChainPassesEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafWildcard.trust + let serverTrustPolicy = ServerTrustPolicy.PerformDefaultEvaluation(validateHost: true) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatExpiredCertificateChainFailsEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafExpired.trust + let serverTrustPolicy = ServerTrustPolicy.PerformDefaultEvaluation(validateHost: true) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatMissingIntermediateCertificateInChainFailsEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSNameMissingIntermediate.trust + let serverTrustPolicy = ServerTrustPolicy.PerformDefaultEvaluation(validateHost: true) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } +} + +// MARK: - + +class ServerTrustPolicyPinCertificatesTestCase: ServerTrustPolicyTestCase { + + // MARK: Validate Certificate Chain Without Validating Host + + func testThatPinnedLeafCertificatePassesEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.LeafValidDNSName] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: true, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinnedIntermediateCertificatePassesEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.IntermediateCA2] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: true, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinnedRootCertificatePassesEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.RootCA] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: true, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningLeafCertificateNotInCertificateChainFailsEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.LeafSignedByCA2] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: true, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatPinningIntermediateCertificateNotInCertificateChainFailsEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.IntermediateCA1] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: true, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatPinningExpiredLeafCertificateFailsEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafExpired.trust + let certificates = [TestCertificates.LeafExpired] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: true, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatPinningIntermediateCertificateWithExpiredLeafCertificateFailsEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafExpired.trust + let certificates = [TestCertificates.IntermediateCA2] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: true, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + // MARK: Validate Certificate Chain and Host + + func testThatPinnedLeafCertificatePassesEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.LeafValidDNSName] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: true, + validateHost: true + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinnedIntermediateCertificatePassesEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.IntermediateCA2] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: true, + validateHost: true + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinnedRootCertificatePassesEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.RootCA] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: true, + validateHost: true + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningLeafCertificateNotInCertificateChainFailsEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.LeafSignedByCA2] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: true, + validateHost: true + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatPinningIntermediateCertificateNotInCertificateChainFailsEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.IntermediateCA1] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: true, + validateHost: true + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatPinningExpiredLeafCertificateFailsEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafExpired.trust + let certificates = [TestCertificates.LeafExpired] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: true, + validateHost: true + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatPinningIntermediateCertificateWithExpiredLeafCertificateFailsEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafExpired.trust + let certificates = [TestCertificates.IntermediateCA2] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: true, + validateHost: true + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + // MARK: Do NOT Validate Certificate Chain or Host + + func testThatPinnedLeafCertificateWithoutCertificateChainValidationPassesEvaluation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.LeafValidDNSName] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: false, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinnedIntermediateCertificateWithoutCertificateChainValidationPassesEvaluation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.IntermediateCA2] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: false, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinnedRootCertificateWithoutCertificateChainValidationPassesEvaluation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.RootCA] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: false, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningLeafCertificateNotInCertificateChainWithoutCertificateChainValidationFailsEvaluation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.LeafSignedByCA2] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: false, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatPinningIntermediateCertificateNotInCertificateChainWithoutCertificateChainValidationFailsEvaluation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let certificates = [TestCertificates.IntermediateCA1] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: false, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatPinningExpiredLeafCertificateWithoutCertificateChainValidationPassesEvaluation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafExpired.trust + let certificates = [TestCertificates.LeafExpired] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: false, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningIntermediateCertificateWithExpiredLeafCertificateWithoutCertificateChainValidationPassesEvaluation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafExpired.trust + let certificates = [TestCertificates.IntermediateCA2] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: false, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningRootCertificateWithExpiredLeafCertificateWithoutCertificateChainValidationPassesEvaluation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafExpired.trust + let certificates = [TestCertificates.RootCA] + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: false, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningMultipleCertificatesWithoutCertificateChainValidationPassesEvaluation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafExpired.trust + + let certificates = [ + TestCertificates.LeafMultipleDNSNames, // not in certificate chain + TestCertificates.LeafSignedByCA1, // not in certificate chain + TestCertificates.LeafExpired, // in certificate chain πŸ‘πŸΌπŸ‘πŸΌ + TestCertificates.LeafWildcard, // not in certificate chain + TestCertificates.LeafDNSNameAndURI, // not in certificate chain + ] + + let serverTrustPolicy = ServerTrustPolicy.PinCertificates( + certificates: certificates, + validateCertificateChain: false, + validateHost: false + ) + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } +} + +// MARK: - + +class ServerTrustPolicyPinPublicKeysTestCase: ServerTrustPolicyTestCase { + + // MARK: Validate Certificate Chain Without Validating Host + + func testThatPinningLeafKeyPassesEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let publicKeys = [TestPublicKeys.LeafValidDNSName] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: true, + validateHost: false + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningIntermediateKeyPassesEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let publicKeys = [TestPublicKeys.IntermediateCA2] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: true, + validateHost: false + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningRootKeyPassesEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let publicKeys = [TestPublicKeys.RootCA] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: true, + validateHost: false + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningKeyNotInCertificateChainFailsEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let publicKeys = [TestPublicKeys.LeafSignedByCA2] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: true, + validateHost: false + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatPinningBackupKeyPassesEvaluationWithoutHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let publicKeys = [TestPublicKeys.LeafSignedByCA1, TestPublicKeys.IntermediateCA1, TestPublicKeys.LeafValidDNSName] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: true, + validateHost: false + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + // MARK: Validate Certificate Chain and Host + + func testThatPinningLeafKeyPassesEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let publicKeys = [TestPublicKeys.LeafValidDNSName] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: true, + validateHost: true + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningIntermediateKeyPassesEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let publicKeys = [TestPublicKeys.IntermediateCA2] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: true, + validateHost: true + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningRootKeyPassesEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let publicKeys = [TestPublicKeys.RootCA] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: true, + validateHost: true + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningKeyNotInCertificateChainFailsEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let publicKeys = [TestPublicKeys.LeafSignedByCA2] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: true, + validateHost: true + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatPinningBackupKeyPassesEvaluationWithHostValidation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let publicKeys = [TestPublicKeys.LeafSignedByCA1, TestPublicKeys.IntermediateCA1, TestPublicKeys.LeafValidDNSName] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: true, + validateHost: true + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + // MARK: Do NOT Validate Certificate Chain or Host + + func testThatPinningLeafKeyWithoutCertificateChainValidationPassesEvaluationWithMissingIntermediateCertificate() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSNameMissingIntermediate.trust + let publicKeys = [TestPublicKeys.LeafValidDNSName] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: false, + validateHost: false + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningRootKeyWithoutCertificateChainValidationFailsEvaluationWithMissingIntermediateCertificate() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSNameMissingIntermediate.trust + let publicKeys = [TestPublicKeys.RootCA] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: false, + validateHost: false + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } + + func testThatPinningLeafKeyWithoutCertificateChainValidationPassesEvaluationWithIncorrectIntermediateCertificate() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSNameWithIncorrectIntermediate.trust + let publicKeys = [TestPublicKeys.LeafValidDNSName] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: false, + validateHost: false + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningLeafKeyWithoutCertificateChainValidationPassesEvaluationWithExpiredLeafCertificate() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafExpired.trust + let publicKeys = [TestPublicKeys.LeafExpired] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: false, + validateHost: false + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningIntermediateKeyWithoutCertificateChainValidationPassesEvaluationWithExpiredLeafCertificate() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafExpired.trust + let publicKeys = [TestPublicKeys.IntermediateCA2] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: false, + validateHost: false + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatPinningRootKeyWithoutCertificateChainValidationPassesEvaluationWithExpiredLeafCertificate() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafExpired.trust + let publicKeys = [TestPublicKeys.RootCA] + let serverTrustPolicy = ServerTrustPolicy.PinPublicKeys( + publicKeys: publicKeys, + validateCertificateChain: false, + validateHost: false + ) + + // When + setRootCertificateAsLoneAnchorCertificateForTrust(serverTrust) + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } +} + +// MARK: - + +class ServerTrustPolicyDisableEvaluationTestCase: ServerTrustPolicyTestCase { + func testThatCertificateChainMissingIntermediateCertificatePassesEvaluation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSNameMissingIntermediate.trust + let serverTrustPolicy = ServerTrustPolicy.DisableEvaluation + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatExpiredLeafCertificatePassesEvaluation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafExpired.trust + let serverTrustPolicy = ServerTrustPolicy.DisableEvaluation + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } +} + +// MARK: - + +class ServerTrustPolicyCustomEvaluationTestCase: ServerTrustPolicyTestCase { + func testThatReturningTrueFromClosurePassesEvaluation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let serverTrustPolicy = ServerTrustPolicy.CustomEvaluation { _, _ in + return true + } + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertTrue(serverTrustIsValid, "server trust should pass evaluation") + } + + func testThatReturningFalseFromClosurePassesEvaluation() { + // Given + let host = "test.alamofire.org" + let serverTrust = TestTrusts.LeafValidDNSName.trust + let serverTrustPolicy = ServerTrustPolicy.CustomEvaluation { _, _ in + return false + } + + // When + let serverTrustIsValid = serverTrustPolicy.evaluateServerTrust(serverTrust, isValidForHost: host) + + // Then + XCTAssertFalse(serverTrustIsValid, "server trust should not pass evaluation") + } +} diff --git a/Tests/TLSEvaluationTests.swift b/Tests/TLSEvaluationTests.swift index 64841cc10..77511ab5c 100644 --- a/Tests/TLSEvaluationTests.swift +++ b/Tests/TLSEvaluationTests.swift @@ -24,16 +24,64 @@ import Alamofire import Foundation import XCTest -class TLSEvaluationTestCase: BaseTestCase { - func testThatExpiredSSLCertificateFailsEvaluation() { - // Given - let URL = "https://testssl-expire.disig.sk/" - let expectation = expectationWithDescription("\(URL)") +private struct TestCertificates { + static let RootCA = TestCertificates.certificateWithFileName("root-ca-disig") + static let IntermediateCA = TestCertificates.certificateWithFileName("intermediate-ca-disig") + static let Leaf = TestCertificates.certificateWithFileName("testssl-expire.disig.sk") + + static func certificateWithFileName(fileName: String) -> SecCertificate { + class Bundle {} + let filePath = NSBundle(forClass: Bundle.self).pathForResource(fileName, ofType: "cer")! + let data = NSData(contentsOfFile: filePath)! + let certificate = SecCertificateCreateWithData(nil, data).takeRetainedValue() + + return certificate + } +} + +// MARK: - + +private struct TestPublicKeys { + static let RootCA = TestPublicKeys.publicKeyForCertificate(TestCertificates.RootCA) + static let IntermediateCA = TestPublicKeys.publicKeyForCertificate(TestCertificates.IntermediateCA) + static let Leaf = TestPublicKeys.publicKeyForCertificate(TestCertificates.Leaf) + + static func publicKeyForCertificate(certificate: SecCertificate) -> SecKey { + let policy = SecPolicyCreateBasicX509().takeRetainedValue() + var unmanagedTrust: Unmanaged? + let trustCreationStatus = SecTrustCreateWithCertificates(certificate, policy, &unmanagedTrust) + + let trust = unmanagedTrust!.takeRetainedValue() + let publicKey = SecTrustCopyPublicKey(trust).takeRetainedValue() + + return publicKey + } +} + +// MARK: - + +class TLSEvaluationExpiredLeafCertificateTestCase: BaseTestCase { + let URL = "https://testssl-expire.disig.sk/" + let host = "testssl-expire.disig.sk" + var configuration: NSURLSessionConfiguration! + + // MARK: Setup and Teardown + override func setUp() { + super.setUp() + self.configuration = NSURLSessionConfiguration.ephemeralSessionConfiguration() + } + + // MARK: Default Behavior Tests + + func testThatExpiredCertificateRequestFailsWithNoServerTrustPolicy() { + // Given + let expectation = expectationWithDescription("\(self.URL)") + let manager = Manager(configuration: self.configuration) var error: NSError? // When - Alamofire.request(.GET, URL) + manager.request(.GET, self.URL) .response { _, _, _, responseError in error = responseError expectation.fulfill() @@ -45,4 +93,379 @@ class TLSEvaluationTestCase: BaseTestCase { XCTAssertNotNil(error, "error should not be nil") XCTAssertEqual(error?.code ?? -1, NSURLErrorServerCertificateUntrusted, "error should be NSURLErrorServerCertificateUntrusted") } + + // MARK: Server Trust Policy - Perform Default Tests + + func testThatExpiredCertificateRequestFailsWithDefaultServerTrustPolicy() { + // Given + let policies = [self.host: ServerTrustPolicy.PerformDefaultEvaluation(validateHost: true)] + let manager = Manager( + configuration: self.configuration, + serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + ) + + let expectation = expectationWithDescription("\(self.URL)") + var error: NSError? + + // When + manager.request(.GET, self.URL) + .response { _, _, _, responseError in + error = responseError + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNotNil(error, "error should not be nil") + XCTAssertEqual(error?.code ?? -1, NSURLErrorCancelled, "error should be NSURLErrorCancelled") + } + + // MARK: Server Trust Policy - Certificate Pinning Tests + + func testThatExpiredCertificateRequestFailsWhenPinningLeafCertificateWithCertificateChainValidation() { + // Given + let certificates = [TestCertificates.Leaf] + let policies: [String: ServerTrustPolicy] = [ + self.host: .PinCertificates(certificates: certificates, validateCertificateChain: true, validateHost: true) + ] + + let manager = Manager( + configuration: self.configuration, + serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + ) + + let expectation = expectationWithDescription("\(self.URL)") + var error: NSError? + + // When + manager.request(.GET, self.URL) + .response { _, _, _, responseError in + error = responseError + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNotNil(error, "error should not be nil") + XCTAssertEqual(error?.code ?? -1, NSURLErrorCancelled, "error should be NSURLErrorCancelled") + } + + func testThatExpiredCertificateRequestFailsWhenPinningAllCertificatesWithCertificateChainValidation() { + // Given + let certificates = [TestCertificates.Leaf, TestCertificates.IntermediateCA, TestCertificates.RootCA] + let policies: [String: ServerTrustPolicy] = [ + self.host: .PinCertificates(certificates: certificates, validateCertificateChain: true, validateHost: true) + ] + + let manager = Manager( + configuration: self.configuration, + serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + ) + + let expectation = expectationWithDescription("\(self.URL)") + var error: NSError? + + // When + manager.request(.GET, self.URL) + .response { _, _, _, responseError in + error = responseError + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNotNil(error, "error should not be nil") + XCTAssertEqual(error?.code ?? -1, NSURLErrorCancelled, "error should be NSURLErrorCancelled") + } + + func testThatExpiredCertificateRequestSucceedsWhenPinningLeafCertificateWithoutCertificateChainValidation() { + // Given + let certificates = [TestCertificates.Leaf] + let policies: [String: ServerTrustPolicy] = [ + self.host: .PinCertificates(certificates: certificates, validateCertificateChain: false, validateHost: true) + ] + + let manager = Manager( + configuration: self.configuration, + serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + ) + + let expectation = expectationWithDescription("\(self.URL)") + var error: NSError? + + // When + manager.request(.GET, self.URL) + .response { _, _, _, responseError in + error = responseError + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNil(error, "error should be nil") + } + + func testThatExpiredCertificateRequestSucceedsWhenPinningIntermediateCACertificateWithoutCertificateChainValidation() { + // Given + let certificates = [TestCertificates.IntermediateCA] + let policies: [String: ServerTrustPolicy] = [ + self.host: .PinCertificates(certificates: certificates, validateCertificateChain: false, validateHost: true) + ] + + let manager = Manager( + configuration: self.configuration, + serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + ) + + let expectation = expectationWithDescription("\(self.URL)") + var error: NSError? + + // When + manager.request(.GET, self.URL) + .response { _, _, _, responseError in + error = responseError + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNil(error, "error should be nil") + } + + func testThatExpiredCertificateRequestSucceedsWhenPinningRootCACertificateWithoutCertificateChainValidation() { + // Given + let certificates = [TestCertificates.RootCA] + let policies: [String: ServerTrustPolicy] = [ + self.host: .PinCertificates(certificates: certificates, validateCertificateChain: false, validateHost: true) + ] + + let manager = Manager( + configuration: self.configuration, + serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + ) + + let expectation = expectationWithDescription("\(self.URL)") + var error: NSError? + + // When + manager.request(.GET, self.URL) + .response { _, _, _, responseError in + error = responseError + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNil(error, "error should be nil") + } + + // MARK: Server Trust Policy - Public Key Pinning Tests + + func testThatExpiredCertificateRequestFailsWhenPinningLeafPublicKeyWithCertificateChainValidation() { + // Given + let publicKeys = [TestPublicKeys.Leaf] + let policies: [String: ServerTrustPolicy] = [ + self.host: .PinPublicKeys(publicKeys: publicKeys, validateCertificateChain: true, validateHost: true) + ] + + let manager = Manager( + configuration: self.configuration, + serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + ) + + let expectation = expectationWithDescription("\(self.URL)") + var error: NSError? + + // When + manager.request(.GET, self.URL) + .response { _, _, _, responseError in + error = responseError + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNotNil(error, "error should not be nil") + XCTAssertEqual(error?.code ?? -1, NSURLErrorCancelled, "error should be NSURLErrorCancelled") + } + + func testThatExpiredCertificateRequestSucceedsWhenPinningLeafPublicKeyWithoutCertificateChainValidation() { + // Given + let publicKeys = [TestPublicKeys.Leaf] + let policies: [String: ServerTrustPolicy] = [ + self.host: .PinPublicKeys(publicKeys: publicKeys, validateCertificateChain: false, validateHost: true) + ] + + let manager = Manager( + configuration: self.configuration, + serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + ) + + let expectation = expectationWithDescription("\(self.URL)") + var error: NSError? + + // When + manager.request(.GET, self.URL) + .response { _, _, _, responseError in + error = responseError + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNil(error, "error should be nil") + } + + func testThatExpiredCertificateRequestSucceedsWhenPinningIntermediateCAPublicKeyWithoutCertificateChainValidation() { + // Given + let publicKeys = [TestPublicKeys.IntermediateCA] + let policies: [String: ServerTrustPolicy] = [ + self.host: .PinPublicKeys(publicKeys: publicKeys, validateCertificateChain: false, validateHost: true) + ] + + let manager = Manager( + configuration: self.configuration, + serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + ) + + let expectation = expectationWithDescription("\(self.URL)") + var error: NSError? + + // When + manager.request(.GET, self.URL) + .response { _, _, _, responseError in + error = responseError + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNil(error, "error should be nil") + } + + func testThatExpiredCertificateRequestSucceedsWhenPinningRootCAPublicKeyWithoutCertificateChainValidation() { + // Given + let publicKeys = [TestPublicKeys.RootCA] + let policies: [String: ServerTrustPolicy] = [ + self.host: .PinPublicKeys(publicKeys: publicKeys, validateCertificateChain: false, validateHost: true) + ] + + let manager = Manager( + configuration: self.configuration, + serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + ) + + let expectation = expectationWithDescription("\(self.URL)") + var error: NSError? + + // When + manager.request(.GET, self.URL) + .response { _, _, _, responseError in + error = responseError + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNil(error, "error should be nil") + } + + // MARK: Server Trust Policy - Disabling Evaluation Tests + + func testThatExpiredCertificateRequestSucceedsWhenDisablingEvaluation() { + // Given + let policies = [self.host: ServerTrustPolicy.DisableEvaluation] + let manager = Manager( + configuration: self.configuration, + serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + ) + + let expectation = expectationWithDescription("\(self.URL)") + var error: NSError? + + // When + manager.request(.GET, self.URL) + .response { _, _, _, responseError in + error = responseError + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNil(error, "error should be nil") + } + + // MARK: Server Trust Policy - Custom Evaluation Tests + + func testThatExpiredCertificateRequestSucceedsWhenCustomEvaluationReturnsTrue() { + // Given + let policies = [ + self.host: ServerTrustPolicy.CustomEvaluation { _, _ in + // Implement a custom evaluation routine here... + return true + } + ] + + let manager = Manager( + configuration: self.configuration, + serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + ) + + let expectation = expectationWithDescription("\(self.URL)") + var error: NSError? + + // When + manager.request(.GET, self.URL) + .response { _, _, _, responseError in + error = responseError + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNil(error, "error should be nil") + } + + func testThatExpiredCertificateRequestFailsWhenCustomEvaluationReturnsFalse() { + // Given + let policies = [ + self.host: ServerTrustPolicy.CustomEvaluation { _, _ in + // Implement a custom evaluation routine here... + return false + } + ] + + let manager = Manager( + configuration: self.configuration, + serverTrustPolicyManager: ServerTrustPolicyManager(policies: policies) + ) + + let expectation = expectationWithDescription("\(self.URL)") + var error: NSError? + + // When + manager.request(.GET, self.URL) + .response { _, _, _, responseError in + error = responseError + expectation.fulfill() + } + + waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) + + // Then + XCTAssertNotNil(error, "error should not be nil") + XCTAssertEqual(error?.code ?? -1, NSURLErrorCancelled, "error should be NSURLErrorCancelled") + } } diff --git a/Tests/URLProtocolTests.swift b/Tests/URLProtocolTests.swift index b65b9c6f0..9fb589b70 100644 --- a/Tests/URLProtocolTests.swift +++ b/Tests/URLProtocolTests.swift @@ -137,26 +137,26 @@ class URLProtocolTestCase: BaseTestCase { var request: NSURLRequest? var response: NSHTTPURLResponse? - var string: AnyObject? + var data: NSData? var error: NSError? // When Alamofire.request(URLRequest) - .response { responseRequest, responseResponse, responseString, responseError in + .response { responseRequest, responseResponse, responseData, responseError in request = responseRequest response = responseResponse - string = responseString + data = responseData error = responseError expectation.fulfill() - } + } waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) // Then XCTAssertNotNil(request, "request should not be nil") XCTAssertNotNil(response, "response should not be nil") - XCTAssertNotNil(string, "string should not be nil") + XCTAssertNotNil(data, "data should not be nil") XCTAssertNil(error, "error should be nil") if let headers = response?.allHeaderFields as? [String: String] { diff --git a/Tests/UploadTests.swift b/Tests/UploadTests.swift index 46d7459ec..6ad2717d5 100644 --- a/Tests/UploadTests.swift +++ b/Tests/UploadTests.swift @@ -186,7 +186,7 @@ class UploadDataTestCase: BaseTestCase { var progressValues: [(completedUnitCount: Int64, totalUnitCount: Int64)] = [] var responseRequest: NSURLRequest? var responseResponse: NSHTTPURLResponse? - var responseData: AnyObject? + var responseData: NSData? var responseError: NSError? // When @@ -259,7 +259,7 @@ class UploadMultipartFormDataTestCase: BaseTestCase { var formData: MultipartFormData? var request: NSURLRequest? var response: NSHTTPURLResponse? - var data: AnyObject? + var data: NSData? var error: NSError? // When @@ -316,7 +316,7 @@ class UploadMultipartFormDataTestCase: BaseTestCase { var request: NSURLRequest? var response: NSHTTPURLResponse? - var data: AnyObject? + var data: NSData? var error: NSError? // When @@ -593,7 +593,7 @@ class UploadMultipartFormDataTestCase: BaseTestCase { var progressValues: [(completedUnitCount: Int64, totalUnitCount: Int64)] = [] var request: NSURLRequest? var response: NSHTTPURLResponse? - var data: AnyObject? + var data: NSData? var error: NSError? // When diff --git a/Tests/ValidationTests.swift b/Tests/ValidationTests.swift index 2231a15b6..9dea6d643 100644 --- a/Tests/ValidationTests.swift +++ b/Tests/ValidationTests.swift @@ -25,7 +25,7 @@ import Foundation import XCTest class StatusCodeValidationTestCase: BaseTestCase { - func testValidationForRequestWithAcceptableStatusCodeResponse() { + func testThatValidationForRequestWithAcceptableStatusCodeResponseSucceeds() { // Given let URL = "http://httpbin.org/status/200" let expectation = expectationWithDescription("\(URL)") @@ -46,7 +46,7 @@ class StatusCodeValidationTestCase: BaseTestCase { XCTAssertNil(error, "error should be nil") } - func testValidationForRequestWithUnacceptableStatusCodeResponse() { + func testThatValidationForRequestWithUnacceptableStatusCodeResponseFails() { // Given let URL = "http://httpbin.org/status/404" let expectation = expectationWithDescription("\(URL)") @@ -68,7 +68,7 @@ class StatusCodeValidationTestCase: BaseTestCase { XCTAssertEqual(error?.domain ?? "", AlamofireErrorDomain, "error should be in Alamofire error domain") } - func testValidationForRequestWithNoAcceptableStatusCodes() { + func testThatValidationForRequestWithNoAcceptableStatusCodesFails() { // Given let URL = "http://httpbin.org/status/201" let expectation = expectationWithDescription("\(URL)") @@ -94,7 +94,7 @@ class StatusCodeValidationTestCase: BaseTestCase { // MARK: - class ContentTypeValidationTestCase: BaseTestCase { - func testValidationForRequestWithAcceptableContentTypeResponse() { + func testThatValidationForRequestWithAcceptableContentTypeResponseSucceeds() { // Given let URL = "http://httpbin.org/ip" let expectation = expectationWithDescription("\(URL)") @@ -115,7 +115,7 @@ class ContentTypeValidationTestCase: BaseTestCase { XCTAssertNil(error, "error should be nil") } - func testValidationForRequestWithAcceptableWildcardContentTypeResponse() { + func testThatValidationForRequestWithAcceptableWildcardContentTypeResponseSucceeds() { // Given let URL = "http://httpbin.org/ip" let expectation = expectationWithDescription("\(URL)") @@ -138,7 +138,7 @@ class ContentTypeValidationTestCase: BaseTestCase { XCTAssertNil(error, "error should be nil") } - func testValidationForRequestWithUnacceptableContentTypeResponse() { + func testThatValidationForRequestWithUnacceptableContentTypeResponseFails() { // Given let URL = "http://httpbin.org/xml" let expectation = expectationWithDescription("\(URL)") @@ -160,7 +160,7 @@ class ContentTypeValidationTestCase: BaseTestCase { XCTAssertEqual(error?.domain ?? "", AlamofireErrorDomain, "error should be in Alamofire error domain") } - func testValidationForRequestWithNoAcceptableContentTypeResponse() { + func testThatValidationForRequestWithNoAcceptableContentTypeResponseFails() { // Given let URL = "http://httpbin.org/xml" let expectation = expectationWithDescription("\(URL)") @@ -186,7 +186,7 @@ class ContentTypeValidationTestCase: BaseTestCase { // MARK: - class MultipleValidationTestCase: BaseTestCase { - func testValidationForRequestWithAcceptableStatusCodeAndContentTypeResponse() { + func testThatValidationForRequestWithAcceptableStatusCodeAndContentTypeResponseSucceeds() { // Given let URL = "http://httpbin.org/ip" let expectation = expectationWithDescription("\(URL)") @@ -208,7 +208,7 @@ class MultipleValidationTestCase: BaseTestCase { XCTAssertNil(error, "error should be nil") } - func testValidationForRequestWithUnacceptableStatusCodeAndContentTypeResponse() { + func testThatValidationForRequestWithUnacceptableStatusCodeAndContentTypeResponseFails() { // Given let URL = "http://httpbin.org/xml" let expectation = expectationWithDescription("\(URL)") @@ -235,7 +235,7 @@ class MultipleValidationTestCase: BaseTestCase { // MARK: - class AutomaticValidationTestCase: BaseTestCase { - func testValidationForRequestWithAcceptableStatusCodeAndContentTypeResponse() { + func testThatValidationForRequestWithAcceptableStatusCodeAndContentTypeResponseSucceeds() { // Given let URL = NSURL(string: "http://httpbin.org/ip")! let mutableURLRequest = NSMutableURLRequest(URL: URL) @@ -259,7 +259,7 @@ class AutomaticValidationTestCase: BaseTestCase { XCTAssertNil(error, "error should be nil") } - func testValidationForRequestWithUnacceptableStatusCodeResponse() { + func testThatValidationForRequestWithUnacceptableStatusCodeResponseFails() { // Given let URL = "http://httpbin.org/status/404" let expectation = expectationWithDescription("\(URL)") @@ -281,7 +281,7 @@ class AutomaticValidationTestCase: BaseTestCase { XCTAssertEqual(error!.domain, AlamofireErrorDomain, "error should be in Alamofire error domain") } - func testValidationForRequestWithAcceptableWildcardContentTypeResponse() { + func testThatValidationForRequestWithAcceptableWildcardContentTypeResponseSucceeds() { // Given let URL = NSURL(string: "http://httpbin.org/ip")! let mutableURLRequest = NSMutableURLRequest(URL: URL) @@ -305,7 +305,7 @@ class AutomaticValidationTestCase: BaseTestCase { XCTAssertNil(error, "error should be nil") } - func testValidationForRequestWithAcceptableComplexContentTypeResponse() { + func testThatValidationForRequestWithAcceptableComplexContentTypeResponseSucceeds() { // Given let URL = NSURL(string: "http://httpbin.org/xml")! let mutableURLRequest = NSMutableURLRequest(URL: URL) @@ -329,7 +329,7 @@ class AutomaticValidationTestCase: BaseTestCase { XCTAssertNil(error, "error should be nil") } - func testValidationForRequestWithUnacceptableContentTypeResponse() { + func testThatValidationForRequestWithUnacceptableContentTypeResponseFails() { // Given let URL = NSURL(string: "http://httpbin.org/xml")! let mutableURLRequest = NSMutableURLRequest(URL: URL) @@ -340,7 +340,7 @@ class AutomaticValidationTestCase: BaseTestCase { var error: NSError? // When - Alamofire.request(.GET, URL) + Alamofire.request(mutableURLRequest) .validate() .response { _, _, _, responseError in error = responseError @@ -350,6 +350,7 @@ class AutomaticValidationTestCase: BaseTestCase { waitForExpectationsWithTimeout(self.defaultTimeout, handler: nil) // Then - XCTAssertNil(error, "error should be nil") + XCTAssertNotNil(error, "error should not be nil") + XCTAssertEqual(error!.domain, AlamofireErrorDomain, "error should be in Alamofire error domain") } }