├── .gitignore ├── Cartfile ├── Cartfile.resolved ├── Config ├── Base.xcconfig ├── FrameworkCommon.xcconfig ├── OBPKit-OSX.xcconfig └── OBPKit-iOS.xcconfig ├── Framework ├── Info.plist └── OBPKit.h ├── GenerateKey └── main.m ├── LICENSE ├── OBPKit-Cart.xcworkspace └── contents.xcworkspacedata ├── OBPKit.podspec ├── OBPKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ ├── OBPKit-OSX.xcscheme │ └── OBPKit-iOS.xcscheme ├── OBPKit ├── Connection │ ├── OBPServerInfo.h │ ├── OBPServerInfo.m │ ├── OBPServerInfoStore.h │ ├── OBPServerInfoStore.m │ ├── OBPSession.h │ ├── OBPSession.m │ ├── OBPWebViewProvider.h │ └── OBPWebViewProvider.m ├── Marshal │ ├── OBPMarshal.h │ └── OBPMarshal.m └── Util │ ├── NSString+OBPKit.h │ ├── NSString+OBPKit.m │ ├── OBPDateFormatter.h │ ├── OBPDateFormatter.m │ ├── OBPLogging.h │ ├── STHTTPRequest+Error.h │ └── STHTTPRequest+Error.m └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata 2 | *.xctimeline 3 | *.xccheckout 4 | *.xcscmblueprint 5 | *.mode2v3 6 | *.pbxuser 7 | (*)/ 8 | .DS_Store 9 | Carthage/ 10 | -------------------------------------------------------------------------------- /Cartfile: -------------------------------------------------------------------------------- 1 | github "t0rst/OAuthCore" ~> 0.0.2 2 | github "nst/STHTTPRequest" ~> 1.1.4 3 | github "kishikawakatsumi/UICKeyChainStore" ~> 2.1.1 4 | -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "kishikawakatsumi/UICKeyChainStore" "v2.1.1" 2 | github "nst/STHTTPRequest" "1.1.4" 3 | github "t0rst/OAuthCore" "0.0.2" 4 | -------------------------------------------------------------------------------- /Config/Base.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // Base.xcconfig 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 03/03/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | 10 | 11 | // Switch off unhelpful help 12 | GCC_WARN_CHECK_SWITCH_STATEMENTS = NO 13 | // ...--> add a default case, and therafter newly added enum cases fly under the radar --> not a solution. 14 | 15 | GCC_WARN_MISSING_PARENTHESES = NO 16 | // ...--> visually balancing multiple nested parentheses is higher cognitive load than understanding operator precedence for most common basics like & v |; for the rest, use parentheses judicially and sparingly to make execution and intent clear. 17 | 18 | -------------------------------------------------------------------------------- /Config/FrameworkCommon.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // FrameworkCommon.xcconfig 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 03/03/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | 10 | 11 | CARTHAGE_BUILD_DIR = $(SRCROOT)/Carthage/Build 12 | CARTHAGE_BUILD_PLATFORM_DIR = $(CARTHAGE_BUILD_DIR)/$(CARTHAGE_NAME_FOR_PLATFORM) 13 | FRAMEWORK_SEARCH_PATHS = $(inherited) $(BUILT_PRODUCTS_DIR) $(CARTHAGE_BUILD_PLATFORM_DIR) 14 | -------------------------------------------------------------------------------- /Config/OBPKit-OSX.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // OBPKit-iOS.xcconfig 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 03/03/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #include "FrameworkCommon.xcconfig" 10 | 11 | 12 | 13 | CARTHAGE_NAME_FOR_PLATFORM = Mac 14 | -------------------------------------------------------------------------------- /Config/OBPKit-iOS.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // OBPKit-OSX.xcconfig 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 03/03/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #include "FrameworkCommon.xcconfig" 10 | 11 | 12 | 13 | CARTHAGE_NAME_FOR_PLATFORM = iOS 14 | -------------------------------------------------------------------------------- /Framework/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.1.3 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSHumanReadableCopyright 24 | Copyright (c) 2016-2107 TESOBE Ltd. All rights reserved. 25 | NSPrincipalClass 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Framework/OBPKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // OBPKit.h 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 22/01/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for OBPKit-OSX. 12 | FOUNDATION_EXPORT double OBPKit_OSXVersionNumber; 13 | 14 | //! Project version string for OBPKit-OSX. 15 | FOUNDATION_EXPORT const unsigned char OBPKit_OSXVersionString[]; 16 | 17 | #import 18 | #import 19 | 20 | #ifndef _OBPKit_h 21 | #define _OBPKit_h 22 | 23 | // Public Headers 24 | #import 25 | #import 26 | #import 27 | #import 28 | #import 29 | #import 30 | #import 31 | #import 32 | 33 | #endif // _OBPKit_h -------------------------------------------------------------------------------- /GenerateKey/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // GenerateKey 4 | // 5 | // Created by Torsten Louland on 17/03/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import 10 | #include 11 | #include 12 | #import "OBPServerInfo.h" 13 | 14 | 15 | 16 | /** GenerateKey 17 | 18 | Utility function to generate a text file defining random bytes for inclusion in source code, for eventual use as a symmetric encryption key and initialisation vector. The amount of bytes generated are more than needed for most encryption algorhythms - just use the number of bytes that are necessary for your choice of encryption algorithm. 19 | 20 | Typical file content: 21 | 22 | #define KEY_LEN 16 23 | #define KEY_BYTES 17, 255, 0, 33, ... 42, 176 24 | #define IV_LEN 16 25 | #define IV_BYTES 38, 187, 113, ... 46, 3 26 | 27 | Example use in an OBPProvideCryptParamsBlock: 28 | 29 | OBPProvideCryptParamsBlock cb = 30 | ^void(OBPCryptParams* ioParams, size_t keySizeMax, size_t ivSizeMax) 31 | { 32 | #include "KeyDef.h" 33 | 34 | enum {kKeyLen = KEY_LEN, kIVLen = IV_LEN}; 35 | const uint8_t key[kKeyLen] = {KEY_BYTES}; 36 | const uint8_t iv[kIVLen] = {IV_BYTES}; 37 | 38 | #undef KEY_LEN 39 | #undef KEY_BYTES 40 | #undef IV_LEN 41 | #undef IV_BYTES 42 | 43 | // ...now make use of key and iv by copying as many bytes as you need for specific algorithm... 44 | 45 | if (ioParams && ioParams->key) 46 | { 47 | size_t len = MIN(MIN(kKeyLen, ioParams->keySize), keySizeMax); 48 | memcpy(ioParams->key, key, len); 49 | ioParams->keySize = len; 50 | } 51 | 52 | if (ioParams && ioParams->iv) 53 | { 54 | size_t len = MIN(MIN(kIVLen, ioParams->blockSize), ivSizeMax); 55 | memcpy(ioParams->iv, iv, len); 56 | ioParams->blockSize = len; 57 | } 58 | }; 59 | 60 | */ 61 | void GenerateKey() 62 | { 63 | enum { 64 | kKeyLenMax = 64, // kOBPClientCredentialMaxCryptKeyLen, 65 | // ...64 is enough for all but RC2 max and RC4 max. 66 | kIVLenMax = kOBPClientCredentialMaxCryptBlockLen, 67 | kBufLen = kKeyLenMax + kIVLenMax, 68 | kShortsInNSTimeInterval = sizeof(NSTimeInterval)/sizeof(uint16_t), 69 | }; 70 | union { 71 | NSTimeInterval ti; 72 | uint16_t shorts[kShortsInNSTimeInterval]; 73 | } now; 74 | char buf[kBufLen]; 75 | const char* sep; 76 | uint16_t n; 77 | NSMutableString* ms; 78 | NSString* path; 79 | 80 | // Discard a variable number of bytes from pseudo-random sequence 81 | now.ti = [NSDate timeIntervalSinceReferenceDate]; 82 | n = (now.shorts[0] ^ now.shorts[kShortsInNSTimeInterval-1]) / kBufLen; 83 | while (n--) 84 | CCRandomGenerateBytes(buf, kBufLen); 85 | n = (now.shorts[0] ^ now.shorts[kShortsInNSTimeInterval-1]) % kBufLen; 86 | if (n) 87 | CCRandomGenerateBytes(buf, n); 88 | 89 | // Get random bytes to use for key 90 | CCRandomGenerateBytes(buf, kKeyLenMax); 91 | 92 | // Skip some more 93 | n = (now.shorts[0] ^ now.shorts[kShortsInNSTimeInterval-1]) / kBufLen; 94 | while (n--) 95 | CCRandomGenerateBytes(buf, kBufLen); 96 | n = (now.shorts[0] ^ now.shorts[kShortsInNSTimeInterval-1]) % kBufLen; 97 | if (n) 98 | CCRandomGenerateBytes(buf, n); 99 | 100 | // Get random bytes to use for IV 101 | CCRandomGenerateBytes(buf+kKeyLenMax, kIVLenMax); 102 | 103 | // Format for storage 104 | ms = [NSMutableString string]; 105 | [ms appendFormat: @"#define KEY_LEN %d\n", kKeyLenMax]; 106 | for (n = 0, sep = "#define KEY_BYTES "; n < kKeyLenMax; n++, sep = ", ") 107 | [ms appendFormat: @"%s%u", sep, (unsigned char)buf[n]]; 108 | [ms appendFormat: @"\n#define IV_LEN %d\n", kIVLenMax]; 109 | for (sep = "#define IV_BYTES "; n < kBufLen; n++, sep = ", ") 110 | [ms appendFormat: @"%s%u", sep, (unsigned char)buf[n]]; 111 | [ms appendString: @"\n"]; 112 | 113 | // Write to ~/Desktop/KeyDef.h 114 | path = NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES)[0]; 115 | path = [path stringByAppendingPathComponent: @"KeyDef.h"]; 116 | [ms writeToFile: path atomically: YES encoding: NSUTF8StringEncoding error: NULL]; 117 | } 118 | 119 | int main(int argc, const char * argv[]) { 120 | @autoreleasepool { 121 | GenerateKey(); 122 | } 123 | return 0; 124 | } 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2021 TESOBE GmbH 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /OBPKit-Cart.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /OBPKit.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "OBPKit" 3 | s.version = "1.1.3" 4 | s.summary = "Ease access to servers offering the Open Bank Project API." 5 | s.description = "OBPKit is quick to integrate into your iOS app or OSX application, and makes authorisation of sessions and marshalling of resources through the Open Bank Project API simple and easy." 6 | s.homepage = "https://github.com/OpenBankProject/OBPKit-iOSX" 7 | s.license = "MIT" 8 | s.authors = { 9 | "Torsten Louland" => "torsten.louland@satisfyingstructures.com" 10 | } 11 | s.ios.deployment_target = "8.0" 12 | s.osx.deployment_target = "10.9" 13 | s.source = { :git => "https://github.com/OpenBankProject/OBPKit-iOSX.git", :tag => s.version } 14 | s.source_files = "OBPKit/**/*.{h,m}", "Framework", "GenerateKey" 15 | s.public_header_files = "OBPKit/Connection/*.h", "OBPKit/Marshal/*.h", "OBPKit/Util/OBP*.h", "OBPKit/Util/NS*.h", "Framework/*.h" 16 | s.preserve_paths = "Config/*.xcconfig" 17 | s.requires_arc = true 18 | s.dependency "STHTTPRequest", "~> 1.1.4" 19 | s.dependency "OAuthCore", "~> 0.0.2" 20 | s.dependency "UICKeyChainStore", "~> 2.1.1" 21 | end 22 | -------------------------------------------------------------------------------- /OBPKit.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | AE15C5121D074DD6004ED657 /* OBPDateFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = AE15C5101D074DD6004ED657 /* OBPDateFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11 | AE15C5131D074DD6004ED657 /* OBPDateFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = AE15C5111D074DD6004ED657 /* OBPDateFormatter.m */; }; 12 | AE2B7A131CB43A390028B03E /* OAuthCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE2B7A111CB43A390028B03E /* OAuthCore.framework */; }; 13 | AE2B7A141CB43A390028B03E /* STHTTPRequest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE2B7A121CB43A390028B03E /* STHTTPRequest.framework */; }; 14 | AE2B7A171CB43A600028B03E /* OAuthCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE2B7A151CB43A600028B03E /* OAuthCore.framework */; }; 15 | AE2B7A181CB43A600028B03E /* STHTTPRequest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE2B7A161CB43A600028B03E /* STHTTPRequest.framework */; }; 16 | AE2B7A1A1CB43C820028B03E /* UICKeyChainStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE2B7A191CB43C820028B03E /* UICKeyChainStore.framework */; }; 17 | AE2B7A1C1CB43C940028B03E /* UICKeyChainStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE2B7A1B1CB43C940028B03E /* UICKeyChainStore.framework */; }; 18 | AE3BC07F1C7F6862001A1AE1 /* OBPServerInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = AE3BC0721C7F6862001A1AE1 /* OBPServerInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 19 | AE3BC0801C7F6862001A1AE1 /* OBPServerInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = AE3BC0721C7F6862001A1AE1 /* OBPServerInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; 20 | AE3BC0811C7F6862001A1AE1 /* OBPServerInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = AE3BC0731C7F6862001A1AE1 /* OBPServerInfo.m */; }; 21 | AE3BC0821C7F6862001A1AE1 /* OBPServerInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = AE3BC0731C7F6862001A1AE1 /* OBPServerInfo.m */; }; 22 | AE3BC0831C7F6862001A1AE1 /* OBPSession.h in Headers */ = {isa = PBXBuildFile; fileRef = AE3BC0741C7F6862001A1AE1 /* OBPSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; 23 | AE3BC0841C7F6862001A1AE1 /* OBPSession.h in Headers */ = {isa = PBXBuildFile; fileRef = AE3BC0741C7F6862001A1AE1 /* OBPSession.h */; settings = {ATTRIBUTES = (Public, ); }; }; 24 | AE3BC0851C7F6862001A1AE1 /* OBPSession.m in Sources */ = {isa = PBXBuildFile; fileRef = AE3BC0751C7F6862001A1AE1 /* OBPSession.m */; }; 25 | AE3BC0861C7F6862001A1AE1 /* OBPSession.m in Sources */ = {isa = PBXBuildFile; fileRef = AE3BC0751C7F6862001A1AE1 /* OBPSession.m */; }; 26 | AE3BC0871C7F6862001A1AE1 /* OBPWebViewProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = AE3BC0761C7F6862001A1AE1 /* OBPWebViewProvider.h */; settings = {ATTRIBUTES = (Public, ); }; }; 27 | AE3BC0881C7F6862001A1AE1 /* OBPWebViewProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = AE3BC0761C7F6862001A1AE1 /* OBPWebViewProvider.h */; settings = {ATTRIBUTES = (Public, ); }; }; 28 | AE3BC0891C7F6862001A1AE1 /* OBPWebViewProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = AE3BC0771C7F6862001A1AE1 /* OBPWebViewProvider.m */; }; 29 | AE3BC08A1C7F6862001A1AE1 /* OBPWebViewProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = AE3BC0771C7F6862001A1AE1 /* OBPWebViewProvider.m */; }; 30 | AE3BC08B1C7F6862001A1AE1 /* OBPMarshal.h in Headers */ = {isa = PBXBuildFile; fileRef = AE3BC0791C7F6862001A1AE1 /* OBPMarshal.h */; settings = {ATTRIBUTES = (Public, ); }; }; 31 | AE3BC08C1C7F6862001A1AE1 /* OBPMarshal.h in Headers */ = {isa = PBXBuildFile; fileRef = AE3BC0791C7F6862001A1AE1 /* OBPMarshal.h */; settings = {ATTRIBUTES = (Public, ); }; }; 32 | AE3BC08D1C7F6862001A1AE1 /* OBPMarshal.m in Sources */ = {isa = PBXBuildFile; fileRef = AE3BC07A1C7F6862001A1AE1 /* OBPMarshal.m */; }; 33 | AE3BC08E1C7F6862001A1AE1 /* OBPMarshal.m in Sources */ = {isa = PBXBuildFile; fileRef = AE3BC07A1C7F6862001A1AE1 /* OBPMarshal.m */; }; 34 | AE3BC08F1C7F6862001A1AE1 /* NSString+OBPKit.h in Headers */ = {isa = PBXBuildFile; fileRef = AE3BC07C1C7F6862001A1AE1 /* NSString+OBPKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 35 | AE3BC0901C7F6862001A1AE1 /* NSString+OBPKit.h in Headers */ = {isa = PBXBuildFile; fileRef = AE3BC07C1C7F6862001A1AE1 /* NSString+OBPKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 36 | AE3BC0911C7F6862001A1AE1 /* NSString+OBPKit.m in Sources */ = {isa = PBXBuildFile; fileRef = AE3BC07D1C7F6862001A1AE1 /* NSString+OBPKit.m */; }; 37 | AE3BC0921C7F6862001A1AE1 /* NSString+OBPKit.m in Sources */ = {isa = PBXBuildFile; fileRef = AE3BC07D1C7F6862001A1AE1 /* NSString+OBPKit.m */; }; 38 | AE3BC0931C7F6862001A1AE1 /* OBPLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = AE3BC07E1C7F6862001A1AE1 /* OBPLogging.h */; settings = {ATTRIBUTES = (Public, ); }; }; 39 | AE3BC0941C7F6862001A1AE1 /* OBPLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = AE3BC07E1C7F6862001A1AE1 /* OBPLogging.h */; settings = {ATTRIBUTES = (Public, ); }; }; 40 | AE3BC09A1C7F6962001A1AE1 /* OBPKit.h in Headers */ = {isa = PBXBuildFile; fileRef = AE3BC0971C7F6962001A1AE1 /* OBPKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 41 | AE3BC09B1C7F6962001A1AE1 /* OBPKit.h in Headers */ = {isa = PBXBuildFile; fileRef = AE3BC0971C7F6962001A1AE1 /* OBPKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 42 | AE8099F21C9AFEA0003C7D4F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AE8099F11C9AFEA0003C7D4F /* main.m */; }; 43 | AE883F941D08A75F00EC2DA4 /* OBPDateFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = AE15C5111D074DD6004ED657 /* OBPDateFormatter.m */; }; 44 | AE883F951D08A76500EC2DA4 /* OBPDateFormatter.h in Headers */ = {isa = PBXBuildFile; fileRef = AE15C5101D074DD6004ED657 /* OBPDateFormatter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 45 | AEA6FF451C98610F005C3A8B /* OBPServerInfoStore.h in Headers */ = {isa = PBXBuildFile; fileRef = AEA6FF431C98610F005C3A8B /* OBPServerInfoStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 46 | AEA6FF461C98610F005C3A8B /* OBPServerInfoStore.h in Headers */ = {isa = PBXBuildFile; fileRef = AEA6FF431C98610F005C3A8B /* OBPServerInfoStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; 47 | AEA6FF471C986110005C3A8B /* OBPServerInfoStore.m in Sources */ = {isa = PBXBuildFile; fileRef = AEA6FF441C98610F005C3A8B /* OBPServerInfoStore.m */; }; 48 | AEA6FF481C986110005C3A8B /* OBPServerInfoStore.m in Sources */ = {isa = PBXBuildFile; fileRef = AEA6FF441C98610F005C3A8B /* OBPServerInfoStore.m */; }; 49 | AEF8813D1D13246A00824B18 /* STHTTPRequest+Error.h in Headers */ = {isa = PBXBuildFile; fileRef = AEF8813B1D13246A00824B18 /* STHTTPRequest+Error.h */; }; 50 | AEF8813E1D13246A00824B18 /* STHTTPRequest+Error.h in Headers */ = {isa = PBXBuildFile; fileRef = AEF8813B1D13246A00824B18 /* STHTTPRequest+Error.h */; }; 51 | AEF8813F1D13246A00824B18 /* STHTTPRequest+Error.m in Sources */ = {isa = PBXBuildFile; fileRef = AEF8813C1D13246A00824B18 /* STHTTPRequest+Error.m */; }; 52 | AEF881401D13246A00824B18 /* STHTTPRequest+Error.m in Sources */ = {isa = PBXBuildFile; fileRef = AEF8813C1D13246A00824B18 /* STHTTPRequest+Error.m */; }; 53 | /* End PBXBuildFile section */ 54 | 55 | /* Begin PBXCopyFilesBuildPhase section */ 56 | AE8099ED1C9AFEA0003C7D4F /* CopyFiles */ = { 57 | isa = PBXCopyFilesBuildPhase; 58 | buildActionMask = 2147483647; 59 | dstPath = /usr/share/man/man1/; 60 | dstSubfolderSpec = 0; 61 | files = ( 62 | ); 63 | runOnlyForDeploymentPostprocessing = 1; 64 | }; 65 | /* End PBXCopyFilesBuildPhase section */ 66 | 67 | /* Begin PBXFileReference section */ 68 | AE1264A21C88495A00AE0B00 /* Base.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Base.xcconfig; sourceTree = ""; }; 69 | AE1264A31C8849DA00AE0B00 /* OBPKit-iOS.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "OBPKit-iOS.xcconfig"; sourceTree = ""; }; 70 | AE1264A61C884B0A00AE0B00 /* FrameworkCommon.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = FrameworkCommon.xcconfig; sourceTree = ""; }; 71 | AE1264A71C884B4100AE0B00 /* OBPKit-OSX.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "OBPKit-OSX.xcconfig"; sourceTree = ""; }; 72 | AE15C5101D074DD6004ED657 /* OBPDateFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OBPDateFormatter.h; sourceTree = ""; }; 73 | AE15C5111D074DD6004ED657 /* OBPDateFormatter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OBPDateFormatter.m; sourceTree = ""; }; 74 | AE2820321CC10C0500BC0AAC /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = ""; }; 75 | AE2820331CC10C0500BC0AAC /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 76 | AE2820341CC10C0500BC0AAC /* OBPKit.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = OBPKit.podspec; sourceTree = ""; }; 77 | AE2B7A111CB43A390028B03E /* OAuthCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OAuthCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 78 | AE2B7A121CB43A390028B03E /* STHTTPRequest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = STHTTPRequest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 79 | AE2B7A151CB43A600028B03E /* OAuthCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OAuthCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 80 | AE2B7A161CB43A600028B03E /* STHTTPRequest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = STHTTPRequest.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 81 | AE2B7A191CB43C820028B03E /* UICKeyChainStore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = UICKeyChainStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 82 | AE2B7A1B1CB43C940028B03E /* UICKeyChainStore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = UICKeyChainStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 83 | AE3BC0721C7F6862001A1AE1 /* OBPServerInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OBPServerInfo.h; sourceTree = ""; }; 84 | AE3BC0731C7F6862001A1AE1 /* OBPServerInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OBPServerInfo.m; sourceTree = ""; }; 85 | AE3BC0741C7F6862001A1AE1 /* OBPSession.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OBPSession.h; sourceTree = ""; }; 86 | AE3BC0751C7F6862001A1AE1 /* OBPSession.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OBPSession.m; sourceTree = ""; }; 87 | AE3BC0761C7F6862001A1AE1 /* OBPWebViewProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OBPWebViewProvider.h; sourceTree = ""; }; 88 | AE3BC0771C7F6862001A1AE1 /* OBPWebViewProvider.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OBPWebViewProvider.m; sourceTree = ""; }; 89 | AE3BC0791C7F6862001A1AE1 /* OBPMarshal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OBPMarshal.h; sourceTree = ""; }; 90 | AE3BC07A1C7F6862001A1AE1 /* OBPMarshal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OBPMarshal.m; sourceTree = ""; }; 91 | AE3BC07C1C7F6862001A1AE1 /* NSString+OBPKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+OBPKit.h"; sourceTree = ""; }; 92 | AE3BC07D1C7F6862001A1AE1 /* NSString+OBPKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+OBPKit.m"; sourceTree = ""; }; 93 | AE3BC07E1C7F6862001A1AE1 /* OBPLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OBPLogging.h; sourceTree = ""; }; 94 | AE3BC0961C7F6962001A1AE1 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 95 | AE3BC0971C7F6962001A1AE1 /* OBPKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OBPKit.h; sourceTree = ""; }; 96 | AE8099EF1C9AFEA0003C7D4F /* GenerateKey */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = GenerateKey; sourceTree = BUILT_PRODUCTS_DIR; }; 97 | AE8099F11C9AFEA0003C7D4F /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 98 | AE87F2A81C5288DE00D09FBC /* OBPKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OBPKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 99 | AE87F2B51C52891A00D09FBC /* OBPKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = OBPKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 100 | AE87F2C21C528BDA00D09FBC /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 101 | AEA6FF431C98610F005C3A8B /* OBPServerInfoStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OBPServerInfoStore.h; sourceTree = ""; }; 102 | AEA6FF441C98610F005C3A8B /* OBPServerInfoStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OBPServerInfoStore.m; sourceTree = ""; }; 103 | AEF8813B1D13246A00824B18 /* STHTTPRequest+Error.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "STHTTPRequest+Error.h"; sourceTree = ""; }; 104 | AEF8813C1D13246A00824B18 /* STHTTPRequest+Error.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "STHTTPRequest+Error.m"; sourceTree = ""; }; 105 | /* End PBXFileReference section */ 106 | 107 | /* Begin PBXFrameworksBuildPhase section */ 108 | AE8099EC1C9AFEA0003C7D4F /* Frameworks */ = { 109 | isa = PBXFrameworksBuildPhase; 110 | buildActionMask = 2147483647; 111 | files = ( 112 | ); 113 | runOnlyForDeploymentPostprocessing = 0; 114 | }; 115 | AE87F2A41C5288DE00D09FBC /* Frameworks */ = { 116 | isa = PBXFrameworksBuildPhase; 117 | buildActionMask = 2147483647; 118 | files = ( 119 | AE2B7A131CB43A390028B03E /* OAuthCore.framework in Frameworks */, 120 | AE2B7A141CB43A390028B03E /* STHTTPRequest.framework in Frameworks */, 121 | AE2B7A1A1CB43C820028B03E /* UICKeyChainStore.framework in Frameworks */, 122 | ); 123 | runOnlyForDeploymentPostprocessing = 0; 124 | }; 125 | AE87F2B11C52891A00D09FBC /* Frameworks */ = { 126 | isa = PBXFrameworksBuildPhase; 127 | buildActionMask = 2147483647; 128 | files = ( 129 | AE2B7A171CB43A600028B03E /* OAuthCore.framework in Frameworks */, 130 | AE2B7A181CB43A600028B03E /* STHTTPRequest.framework in Frameworks */, 131 | AE2B7A1C1CB43C940028B03E /* UICKeyChainStore.framework in Frameworks */, 132 | ); 133 | runOnlyForDeploymentPostprocessing = 0; 134 | }; 135 | /* End PBXFrameworksBuildPhase section */ 136 | 137 | /* Begin PBXGroup section */ 138 | AE2820351CC10C1F00BC0AAC /* Supporting */ = { 139 | isa = PBXGroup; 140 | children = ( 141 | AE2820331CC10C0500BC0AAC /* LICENSE */, 142 | AE2820321CC10C0500BC0AAC /* Cartfile */, 143 | AE2820341CC10C0500BC0AAC /* OBPKit.podspec */, 144 | ); 145 | name = Supporting; 146 | sourceTree = ""; 147 | }; 148 | AE2B7A0E1CB4399C0028B03E /* Frameworks */ = { 149 | isa = PBXGroup; 150 | children = ( 151 | AE2B7A151CB43A600028B03E /* OAuthCore.framework */, 152 | AE2B7A161CB43A600028B03E /* STHTTPRequest.framework */, 153 | AE2B7A1B1CB43C940028B03E /* UICKeyChainStore.framework */, 154 | AE2B7A111CB43A390028B03E /* OAuthCore.framework */, 155 | AE2B7A191CB43C820028B03E /* UICKeyChainStore.framework */, 156 | AE2B7A121CB43A390028B03E /* STHTTPRequest.framework */, 157 | ); 158 | name = Frameworks; 159 | sourceTree = BUILT_PRODUCTS_DIR; 160 | }; 161 | AE3BC0701C7F6862001A1AE1 /* OBPKit */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | AE3BC0711C7F6862001A1AE1 /* Connection */, 165 | AE3BC0781C7F6862001A1AE1 /* Marshal */, 166 | AE3BC07B1C7F6862001A1AE1 /* Util */, 167 | ); 168 | path = OBPKit; 169 | sourceTree = ""; 170 | }; 171 | AE3BC0711C7F6862001A1AE1 /* Connection */ = { 172 | isa = PBXGroup; 173 | children = ( 174 | AE3BC0721C7F6862001A1AE1 /* OBPServerInfo.h */, 175 | AE3BC0731C7F6862001A1AE1 /* OBPServerInfo.m */, 176 | AE3BC0741C7F6862001A1AE1 /* OBPSession.h */, 177 | AE3BC0751C7F6862001A1AE1 /* OBPSession.m */, 178 | AE3BC0761C7F6862001A1AE1 /* OBPWebViewProvider.h */, 179 | AE3BC0771C7F6862001A1AE1 /* OBPWebViewProvider.m */, 180 | AEA6FF431C98610F005C3A8B /* OBPServerInfoStore.h */, 181 | AEA6FF441C98610F005C3A8B /* OBPServerInfoStore.m */, 182 | ); 183 | path = Connection; 184 | sourceTree = ""; 185 | }; 186 | AE3BC0781C7F6862001A1AE1 /* Marshal */ = { 187 | isa = PBXGroup; 188 | children = ( 189 | AE3BC0791C7F6862001A1AE1 /* OBPMarshal.h */, 190 | AE3BC07A1C7F6862001A1AE1 /* OBPMarshal.m */, 191 | ); 192 | path = Marshal; 193 | sourceTree = ""; 194 | }; 195 | AE3BC07B1C7F6862001A1AE1 /* Util */ = { 196 | isa = PBXGroup; 197 | children = ( 198 | AE3BC07C1C7F6862001A1AE1 /* NSString+OBPKit.h */, 199 | AE3BC07D1C7F6862001A1AE1 /* NSString+OBPKit.m */, 200 | AE15C5101D074DD6004ED657 /* OBPDateFormatter.h */, 201 | AE15C5111D074DD6004ED657 /* OBPDateFormatter.m */, 202 | AE3BC07E1C7F6862001A1AE1 /* OBPLogging.h */, 203 | AEF8813B1D13246A00824B18 /* STHTTPRequest+Error.h */, 204 | AEF8813C1D13246A00824B18 /* STHTTPRequest+Error.m */, 205 | ); 206 | path = Util; 207 | sourceTree = ""; 208 | }; 209 | AE3BC0951C7F6962001A1AE1 /* Framework */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | AE3BC0971C7F6962001A1AE1 /* OBPKit.h */, 213 | AE3BC0961C7F6962001A1AE1 /* Info.plist */, 214 | ); 215 | path = Framework; 216 | sourceTree = ""; 217 | }; 218 | AE8099F01C9AFEA0003C7D4F /* GenerateKey */ = { 219 | isa = PBXGroup; 220 | children = ( 221 | AE8099F11C9AFEA0003C7D4F /* main.m */, 222 | ); 223 | path = GenerateKey; 224 | sourceTree = ""; 225 | }; 226 | AE87F28E1C52889100D09FBC = { 227 | isa = PBXGroup; 228 | children = ( 229 | AE87F2C21C528BDA00D09FBC /* README.md */, 230 | AE3BC0701C7F6862001A1AE1 /* OBPKit */, 231 | AE87F2BD1C528B1300D09FBC /* Config */, 232 | AE3BC0951C7F6962001A1AE1 /* Framework */, 233 | AE8099F01C9AFEA0003C7D4F /* GenerateKey */, 234 | AE2820351CC10C1F00BC0AAC /* Supporting */, 235 | AE2B7A0E1CB4399C0028B03E /* Frameworks */, 236 | AE87F2991C52889100D09FBC /* Products */, 237 | ); 238 | sourceTree = ""; 239 | }; 240 | AE87F2991C52889100D09FBC /* Products */ = { 241 | isa = PBXGroup; 242 | children = ( 243 | AE87F2A81C5288DE00D09FBC /* OBPKit.framework */, 244 | AE87F2B51C52891A00D09FBC /* OBPKit.framework */, 245 | AE8099EF1C9AFEA0003C7D4F /* GenerateKey */, 246 | ); 247 | name = Products; 248 | sourceTree = ""; 249 | }; 250 | AE87F2BD1C528B1300D09FBC /* Config */ = { 251 | isa = PBXGroup; 252 | children = ( 253 | AE1264A21C88495A00AE0B00 /* Base.xcconfig */, 254 | AE1264A61C884B0A00AE0B00 /* FrameworkCommon.xcconfig */, 255 | AE1264A31C8849DA00AE0B00 /* OBPKit-iOS.xcconfig */, 256 | AE1264A71C884B4100AE0B00 /* OBPKit-OSX.xcconfig */, 257 | ); 258 | path = Config; 259 | sourceTree = ""; 260 | }; 261 | /* End PBXGroup section */ 262 | 263 | /* Begin PBXHeadersBuildPhase section */ 264 | AE87F2A51C5288DE00D09FBC /* Headers */ = { 265 | isa = PBXHeadersBuildPhase; 266 | buildActionMask = 2147483647; 267 | files = ( 268 | AE3BC09A1C7F6962001A1AE1 /* OBPKit.h in Headers */, 269 | AE15C5121D074DD6004ED657 /* OBPDateFormatter.h in Headers */, 270 | AE3BC0871C7F6862001A1AE1 /* OBPWebViewProvider.h in Headers */, 271 | AE3BC0931C7F6862001A1AE1 /* OBPLogging.h in Headers */, 272 | AEF8813D1D13246A00824B18 /* STHTTPRequest+Error.h in Headers */, 273 | AE3BC0831C7F6862001A1AE1 /* OBPSession.h in Headers */, 274 | AEA6FF451C98610F005C3A8B /* OBPServerInfoStore.h in Headers */, 275 | AE3BC08B1C7F6862001A1AE1 /* OBPMarshal.h in Headers */, 276 | AE3BC08F1C7F6862001A1AE1 /* NSString+OBPKit.h in Headers */, 277 | AE3BC07F1C7F6862001A1AE1 /* OBPServerInfo.h in Headers */, 278 | ); 279 | runOnlyForDeploymentPostprocessing = 0; 280 | }; 281 | AE87F2B21C52891A00D09FBC /* Headers */ = { 282 | isa = PBXHeadersBuildPhase; 283 | buildActionMask = 2147483647; 284 | files = ( 285 | AE3BC0901C7F6862001A1AE1 /* NSString+OBPKit.h in Headers */, 286 | AE883F951D08A76500EC2DA4 /* OBPDateFormatter.h in Headers */, 287 | AE3BC0941C7F6862001A1AE1 /* OBPLogging.h in Headers */, 288 | AE3BC0881C7F6862001A1AE1 /* OBPWebViewProvider.h in Headers */, 289 | AEF8813E1D13246A00824B18 /* STHTTPRequest+Error.h in Headers */, 290 | AE3BC0801C7F6862001A1AE1 /* OBPServerInfo.h in Headers */, 291 | AEA6FF461C98610F005C3A8B /* OBPServerInfoStore.h in Headers */, 292 | AE3BC0841C7F6862001A1AE1 /* OBPSession.h in Headers */, 293 | AE3BC08C1C7F6862001A1AE1 /* OBPMarshal.h in Headers */, 294 | AE3BC09B1C7F6962001A1AE1 /* OBPKit.h in Headers */, 295 | ); 296 | runOnlyForDeploymentPostprocessing = 0; 297 | }; 298 | /* End PBXHeadersBuildPhase section */ 299 | 300 | /* Begin PBXNativeTarget section */ 301 | AE8099EE1C9AFEA0003C7D4F /* GenerateKey */ = { 302 | isa = PBXNativeTarget; 303 | buildConfigurationList = AE8099F51C9AFEA0003C7D4F /* Build configuration list for PBXNativeTarget "GenerateKey" */; 304 | buildPhases = ( 305 | AE8099EB1C9AFEA0003C7D4F /* Sources */, 306 | AE8099EC1C9AFEA0003C7D4F /* Frameworks */, 307 | AE8099ED1C9AFEA0003C7D4F /* CopyFiles */, 308 | ); 309 | buildRules = ( 310 | ); 311 | dependencies = ( 312 | ); 313 | name = GenerateKey; 314 | productName = GenKey; 315 | productReference = AE8099EF1C9AFEA0003C7D4F /* GenerateKey */; 316 | productType = "com.apple.product-type.tool"; 317 | }; 318 | AE87F2A71C5288DE00D09FBC /* OBPKit-iOS */ = { 319 | isa = PBXNativeTarget; 320 | buildConfigurationList = AE87F2AD1C5288DE00D09FBC /* Build configuration list for PBXNativeTarget "OBPKit-iOS" */; 321 | buildPhases = ( 322 | AE2B7A1D1CB449EC0028B03E /* Make Carthage Build Directory */, 323 | AE87F2A31C5288DE00D09FBC /* Sources */, 324 | AE87F2A41C5288DE00D09FBC /* Frameworks */, 325 | AE87F2A51C5288DE00D09FBC /* Headers */, 326 | AE87F2A61C5288DE00D09FBC /* Resources */, 327 | ); 328 | buildRules = ( 329 | ); 330 | dependencies = ( 331 | ); 332 | name = "OBPKit-iOS"; 333 | productName = "OBPKit-iOS"; 334 | productReference = AE87F2A81C5288DE00D09FBC /* OBPKit.framework */; 335 | productType = "com.apple.product-type.framework"; 336 | }; 337 | AE87F2B41C52891A00D09FBC /* OBPKit-OSX */ = { 338 | isa = PBXNativeTarget; 339 | buildConfigurationList = AE87F2BA1C52891A00D09FBC /* Build configuration list for PBXNativeTarget "OBPKit-OSX" */; 340 | buildPhases = ( 341 | AE2B7A1E1CB44A070028B03E /* Make Carthage Build Directory */, 342 | AE87F2B01C52891A00D09FBC /* Sources */, 343 | AE87F2B11C52891A00D09FBC /* Frameworks */, 344 | AE87F2B21C52891A00D09FBC /* Headers */, 345 | AE87F2B31C52891A00D09FBC /* Resources */, 346 | ); 347 | buildRules = ( 348 | ); 349 | dependencies = ( 350 | ); 351 | name = "OBPKit-OSX"; 352 | productName = "OBPKit-OSX"; 353 | productReference = AE87F2B51C52891A00D09FBC /* OBPKit.framework */; 354 | productType = "com.apple.product-type.framework"; 355 | }; 356 | /* End PBXNativeTarget section */ 357 | 358 | /* Begin PBXProject section */ 359 | AE87F28F1C52889100D09FBC /* Project object */ = { 360 | isa = PBXProject; 361 | attributes = { 362 | LastUpgradeCheck = 0940; 363 | ORGANIZATIONNAME = "TESOBE Ltd"; 364 | TargetAttributes = { 365 | AE8099EE1C9AFEA0003C7D4F = { 366 | CreatedOnToolsVersion = 7.2; 367 | }; 368 | AE87F2A71C5288DE00D09FBC = { 369 | CreatedOnToolsVersion = 7.2; 370 | }; 371 | AE87F2B41C52891A00D09FBC = { 372 | CreatedOnToolsVersion = 7.2; 373 | }; 374 | }; 375 | }; 376 | buildConfigurationList = AE87F2921C52889100D09FBC /* Build configuration list for PBXProject "OBPKit" */; 377 | compatibilityVersion = "Xcode 3.2"; 378 | developmentRegion = English; 379 | hasScannedForEncodings = 0; 380 | knownRegions = ( 381 | en, 382 | ); 383 | mainGroup = AE87F28E1C52889100D09FBC; 384 | productRefGroup = AE87F2991C52889100D09FBC /* Products */; 385 | projectDirPath = ""; 386 | projectRoot = ""; 387 | targets = ( 388 | AE87F2A71C5288DE00D09FBC /* OBPKit-iOS */, 389 | AE87F2B41C52891A00D09FBC /* OBPKit-OSX */, 390 | AE8099EE1C9AFEA0003C7D4F /* GenerateKey */, 391 | ); 392 | }; 393 | /* End PBXProject section */ 394 | 395 | /* Begin PBXResourcesBuildPhase section */ 396 | AE87F2A61C5288DE00D09FBC /* Resources */ = { 397 | isa = PBXResourcesBuildPhase; 398 | buildActionMask = 2147483647; 399 | files = ( 400 | ); 401 | runOnlyForDeploymentPostprocessing = 0; 402 | }; 403 | AE87F2B31C52891A00D09FBC /* Resources */ = { 404 | isa = PBXResourcesBuildPhase; 405 | buildActionMask = 2147483647; 406 | files = ( 407 | ); 408 | runOnlyForDeploymentPostprocessing = 0; 409 | }; 410 | /* End PBXResourcesBuildPhase section */ 411 | 412 | /* Begin PBXShellScriptBuildPhase section */ 413 | AE2B7A1D1CB449EC0028B03E /* Make Carthage Build Directory */ = { 414 | isa = PBXShellScriptBuildPhase; 415 | buildActionMask = 2147483647; 416 | files = ( 417 | ); 418 | inputPaths = ( 419 | ); 420 | name = "Make Carthage Build Directory"; 421 | outputPaths = ( 422 | ); 423 | runOnlyForDeploymentPostprocessing = 0; 424 | shellPath = /bin/sh; 425 | shellScript = "if [ ! -e \"${CARTHAGE_BUILD_PLATFORM_DIR}\" ] ; then\n mkdir -p \"${CARTHAGE_BUILD_PLATFORM_DIR}\"\nfi"; 426 | }; 427 | AE2B7A1E1CB44A070028B03E /* Make Carthage Build Directory */ = { 428 | isa = PBXShellScriptBuildPhase; 429 | buildActionMask = 2147483647; 430 | files = ( 431 | ); 432 | inputPaths = ( 433 | ); 434 | name = "Make Carthage Build Directory"; 435 | outputPaths = ( 436 | ); 437 | runOnlyForDeploymentPostprocessing = 0; 438 | shellPath = /bin/sh; 439 | shellScript = "if [ ! -e \"${CARTHAGE_BUILD_PLATFORM_DIR}\" ] ; then\n mkdir -p \"${CARTHAGE_BUILD_PLATFORM_DIR}\"\nfi"; 440 | }; 441 | /* End PBXShellScriptBuildPhase section */ 442 | 443 | /* Begin PBXSourcesBuildPhase section */ 444 | AE8099EB1C9AFEA0003C7D4F /* Sources */ = { 445 | isa = PBXSourcesBuildPhase; 446 | buildActionMask = 2147483647; 447 | files = ( 448 | AE8099F21C9AFEA0003C7D4F /* main.m in Sources */, 449 | ); 450 | runOnlyForDeploymentPostprocessing = 0; 451 | }; 452 | AE87F2A31C5288DE00D09FBC /* Sources */ = { 453 | isa = PBXSourcesBuildPhase; 454 | buildActionMask = 2147483647; 455 | files = ( 456 | AE3BC0811C7F6862001A1AE1 /* OBPServerInfo.m in Sources */, 457 | AE3BC0851C7F6862001A1AE1 /* OBPSession.m in Sources */, 458 | AE3BC0891C7F6862001A1AE1 /* OBPWebViewProvider.m in Sources */, 459 | AE3BC08D1C7F6862001A1AE1 /* OBPMarshal.m in Sources */, 460 | AE15C5131D074DD6004ED657 /* OBPDateFormatter.m in Sources */, 461 | AEF8813F1D13246A00824B18 /* STHTTPRequest+Error.m in Sources */, 462 | AE3BC0911C7F6862001A1AE1 /* NSString+OBPKit.m in Sources */, 463 | AEA6FF471C986110005C3A8B /* OBPServerInfoStore.m in Sources */, 464 | ); 465 | runOnlyForDeploymentPostprocessing = 0; 466 | }; 467 | AE87F2B01C52891A00D09FBC /* Sources */ = { 468 | isa = PBXSourcesBuildPhase; 469 | buildActionMask = 2147483647; 470 | files = ( 471 | AE3BC0821C7F6862001A1AE1 /* OBPServerInfo.m in Sources */, 472 | AE3BC0861C7F6862001A1AE1 /* OBPSession.m in Sources */, 473 | AE3BC08A1C7F6862001A1AE1 /* OBPWebViewProvider.m in Sources */, 474 | AE3BC08E1C7F6862001A1AE1 /* OBPMarshal.m in Sources */, 475 | AE883F941D08A75F00EC2DA4 /* OBPDateFormatter.m in Sources */, 476 | AEF881401D13246A00824B18 /* STHTTPRequest+Error.m in Sources */, 477 | AE3BC0921C7F6862001A1AE1 /* NSString+OBPKit.m in Sources */, 478 | AEA6FF481C986110005C3A8B /* OBPServerInfoStore.m in Sources */, 479 | ); 480 | runOnlyForDeploymentPostprocessing = 0; 481 | }; 482 | /* End PBXSourcesBuildPhase section */ 483 | 484 | /* Begin XCBuildConfiguration section */ 485 | AE8099F31C9AFEA0003C7D4F /* Debug */ = { 486 | isa = XCBuildConfiguration; 487 | buildSettings = { 488 | CLANG_WARN_UNREACHABLE_CODE = YES; 489 | CODE_SIGN_IDENTITY = "-"; 490 | PRODUCT_NAME = "$(TARGET_NAME)"; 491 | SDKROOT = macosx; 492 | }; 493 | name = Debug; 494 | }; 495 | AE8099F41C9AFEA0003C7D4F /* Release */ = { 496 | isa = XCBuildConfiguration; 497 | buildSettings = { 498 | CLANG_WARN_UNREACHABLE_CODE = YES; 499 | CODE_SIGN_IDENTITY = "-"; 500 | PRODUCT_NAME = "$(TARGET_NAME)"; 501 | SDKROOT = macosx; 502 | }; 503 | name = Release; 504 | }; 505 | AE87F29E1C52889100D09FBC /* Debug */ = { 506 | isa = XCBuildConfiguration; 507 | baseConfigurationReference = AE1264A21C88495A00AE0B00 /* Base.xcconfig */; 508 | buildSettings = { 509 | ALWAYS_SEARCH_USER_PATHS = NO; 510 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 511 | CLANG_CXX_LIBRARY = "libc++"; 512 | CLANG_ENABLE_MODULES = YES; 513 | CLANG_ENABLE_OBJC_ARC = YES; 514 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 515 | CLANG_WARN_BOOL_CONVERSION = YES; 516 | CLANG_WARN_CONSTANT_CONVERSION = YES; 517 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 518 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 519 | CLANG_WARN_EMPTY_BODY = YES; 520 | CLANG_WARN_ENUM_CONVERSION = YES; 521 | CLANG_WARN_INFINITE_RECURSION = YES; 522 | CLANG_WARN_INT_CONVERSION = YES; 523 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 524 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 525 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 526 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 527 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 528 | CLANG_WARN_STRICT_PROTOTYPES = YES; 529 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 530 | CLANG_WARN_UNREACHABLE_CODE = YES; 531 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 532 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 533 | COPY_PHASE_STRIP = NO; 534 | CURRENT_PROJECT_VERSION = 1; 535 | DEBUG_INFORMATION_FORMAT = dwarf; 536 | ENABLE_STRICT_OBJC_MSGSEND = YES; 537 | ENABLE_TESTABILITY = YES; 538 | GCC_C_LANGUAGE_STANDARD = gnu99; 539 | GCC_DYNAMIC_NO_PIC = NO; 540 | GCC_NO_COMMON_BLOCKS = YES; 541 | GCC_OPTIMIZATION_LEVEL = 0; 542 | GCC_PREPROCESSOR_DEFINITIONS = ( 543 | "DEBUG=1", 544 | "$(inherited)", 545 | ); 546 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 547 | GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; 548 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 549 | GCC_WARN_UNDECLARED_SELECTOR = YES; 550 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 551 | GCC_WARN_UNUSED_FUNCTION = YES; 552 | GCC_WARN_UNUSED_VARIABLE = YES; 553 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 554 | MACOSX_DEPLOYMENT_TARGET = 10.10; 555 | MTL_ENABLE_DEBUG_INFO = YES; 556 | ONLY_ACTIVE_ARCH = YES; 557 | SDKROOT = iphoneos; 558 | TARGETED_DEVICE_FAMILY = "1,2"; 559 | VERSIONING_SYSTEM = "apple-generic"; 560 | VERSION_INFO_PREFIX = ""; 561 | }; 562 | name = Debug; 563 | }; 564 | AE87F29F1C52889100D09FBC /* Release */ = { 565 | isa = XCBuildConfiguration; 566 | baseConfigurationReference = AE1264A21C88495A00AE0B00 /* Base.xcconfig */; 567 | buildSettings = { 568 | ALWAYS_SEARCH_USER_PATHS = NO; 569 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 570 | CLANG_CXX_LIBRARY = "libc++"; 571 | CLANG_ENABLE_MODULES = YES; 572 | CLANG_ENABLE_OBJC_ARC = YES; 573 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 574 | CLANG_WARN_BOOL_CONVERSION = YES; 575 | CLANG_WARN_CONSTANT_CONVERSION = YES; 576 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 577 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 578 | CLANG_WARN_EMPTY_BODY = YES; 579 | CLANG_WARN_ENUM_CONVERSION = YES; 580 | CLANG_WARN_INFINITE_RECURSION = YES; 581 | CLANG_WARN_INT_CONVERSION = YES; 582 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 583 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 584 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 585 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 586 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 587 | CLANG_WARN_STRICT_PROTOTYPES = YES; 588 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 589 | CLANG_WARN_UNREACHABLE_CODE = YES; 590 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 591 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 592 | COPY_PHASE_STRIP = NO; 593 | CURRENT_PROJECT_VERSION = 1; 594 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 595 | ENABLE_NS_ASSERTIONS = NO; 596 | ENABLE_STRICT_OBJC_MSGSEND = YES; 597 | GCC_C_LANGUAGE_STANDARD = gnu99; 598 | GCC_NO_COMMON_BLOCKS = YES; 599 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 600 | GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES; 601 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 602 | GCC_WARN_UNDECLARED_SELECTOR = YES; 603 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 604 | GCC_WARN_UNUSED_FUNCTION = YES; 605 | GCC_WARN_UNUSED_VARIABLE = YES; 606 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 607 | MACOSX_DEPLOYMENT_TARGET = 10.10; 608 | MTL_ENABLE_DEBUG_INFO = NO; 609 | SDKROOT = iphoneos; 610 | TARGETED_DEVICE_FAMILY = "1,2"; 611 | VALIDATE_PRODUCT = YES; 612 | VERSIONING_SYSTEM = "apple-generic"; 613 | VERSION_INFO_PREFIX = ""; 614 | }; 615 | name = Release; 616 | }; 617 | AE87F2AE1C5288DE00D09FBC /* Debug */ = { 618 | isa = XCBuildConfiguration; 619 | baseConfigurationReference = AE1264A31C8849DA00AE0B00 /* OBPKit-iOS.xcconfig */; 620 | buildSettings = { 621 | CODE_SIGN_IDENTITY = ""; 622 | DEFINES_MODULE = YES; 623 | DYLIB_COMPATIBILITY_VERSION = 1; 624 | DYLIB_CURRENT_VERSION = 1; 625 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 626 | INFOPLIST_FILE = Framework/Info.plist; 627 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 628 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 629 | PRODUCT_BUNDLE_IDENTIFIER = com.tesobe.OBPKit; 630 | PRODUCT_NAME = OBPKit; 631 | SKIP_INSTALL = YES; 632 | }; 633 | name = Debug; 634 | }; 635 | AE87F2AF1C5288DE00D09FBC /* Release */ = { 636 | isa = XCBuildConfiguration; 637 | baseConfigurationReference = AE1264A31C8849DA00AE0B00 /* OBPKit-iOS.xcconfig */; 638 | buildSettings = { 639 | CODE_SIGN_IDENTITY = ""; 640 | DEFINES_MODULE = YES; 641 | DYLIB_COMPATIBILITY_VERSION = 1; 642 | DYLIB_CURRENT_VERSION = 1; 643 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 644 | INFOPLIST_FILE = Framework/Info.plist; 645 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 646 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 647 | PRODUCT_BUNDLE_IDENTIFIER = com.tesobe.OBPKit; 648 | PRODUCT_NAME = OBPKit; 649 | SKIP_INSTALL = YES; 650 | }; 651 | name = Release; 652 | }; 653 | AE87F2BB1C52891A00D09FBC /* Debug */ = { 654 | isa = XCBuildConfiguration; 655 | baseConfigurationReference = AE1264A71C884B4100AE0B00 /* OBPKit-OSX.xcconfig */; 656 | buildSettings = { 657 | CODE_SIGN_IDENTITY = "-"; 658 | COMBINE_HIDPI_IMAGES = YES; 659 | DEFINES_MODULE = YES; 660 | DYLIB_COMPATIBILITY_VERSION = 1; 661 | DYLIB_CURRENT_VERSION = 1; 662 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 663 | FRAMEWORK_VERSION = A; 664 | INFOPLIST_FILE = Framework/Info.plist; 665 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 666 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 667 | MACOSX_DEPLOYMENT_TARGET = 10.10; 668 | PRODUCT_BUNDLE_IDENTIFIER = com.tesobe.OBPKit; 669 | PRODUCT_NAME = OBPKit; 670 | SDKROOT = macosx; 671 | SKIP_INSTALL = YES; 672 | }; 673 | name = Debug; 674 | }; 675 | AE87F2BC1C52891A00D09FBC /* Release */ = { 676 | isa = XCBuildConfiguration; 677 | baseConfigurationReference = AE1264A71C884B4100AE0B00 /* OBPKit-OSX.xcconfig */; 678 | buildSettings = { 679 | CODE_SIGN_IDENTITY = "-"; 680 | COMBINE_HIDPI_IMAGES = YES; 681 | DEFINES_MODULE = YES; 682 | DYLIB_COMPATIBILITY_VERSION = 1; 683 | DYLIB_CURRENT_VERSION = 1; 684 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 685 | FRAMEWORK_VERSION = A; 686 | INFOPLIST_FILE = Framework/Info.plist; 687 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 688 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; 689 | MACOSX_DEPLOYMENT_TARGET = 10.10; 690 | PRODUCT_BUNDLE_IDENTIFIER = com.tesobe.OBPKit; 691 | PRODUCT_NAME = OBPKit; 692 | SDKROOT = macosx; 693 | SKIP_INSTALL = YES; 694 | }; 695 | name = Release; 696 | }; 697 | /* End XCBuildConfiguration section */ 698 | 699 | /* Begin XCConfigurationList section */ 700 | AE8099F51C9AFEA0003C7D4F /* Build configuration list for PBXNativeTarget "GenerateKey" */ = { 701 | isa = XCConfigurationList; 702 | buildConfigurations = ( 703 | AE8099F31C9AFEA0003C7D4F /* Debug */, 704 | AE8099F41C9AFEA0003C7D4F /* Release */, 705 | ); 706 | defaultConfigurationIsVisible = 0; 707 | defaultConfigurationName = Release; 708 | }; 709 | AE87F2921C52889100D09FBC /* Build configuration list for PBXProject "OBPKit" */ = { 710 | isa = XCConfigurationList; 711 | buildConfigurations = ( 712 | AE87F29E1C52889100D09FBC /* Debug */, 713 | AE87F29F1C52889100D09FBC /* Release */, 714 | ); 715 | defaultConfigurationIsVisible = 0; 716 | defaultConfigurationName = Release; 717 | }; 718 | AE87F2AD1C5288DE00D09FBC /* Build configuration list for PBXNativeTarget "OBPKit-iOS" */ = { 719 | isa = XCConfigurationList; 720 | buildConfigurations = ( 721 | AE87F2AE1C5288DE00D09FBC /* Debug */, 722 | AE87F2AF1C5288DE00D09FBC /* Release */, 723 | ); 724 | defaultConfigurationIsVisible = 0; 725 | defaultConfigurationName = Release; 726 | }; 727 | AE87F2BA1C52891A00D09FBC /* Build configuration list for PBXNativeTarget "OBPKit-OSX" */ = { 728 | isa = XCConfigurationList; 729 | buildConfigurations = ( 730 | AE87F2BB1C52891A00D09FBC /* Debug */, 731 | AE87F2BC1C52891A00D09FBC /* Release */, 732 | ); 733 | defaultConfigurationIsVisible = 0; 734 | defaultConfigurationName = Release; 735 | }; 736 | /* End XCConfigurationList section */ 737 | }; 738 | rootObject = AE87F28F1C52889100D09FBC /* Project object */; 739 | } 740 | -------------------------------------------------------------------------------- /OBPKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /OBPKit.xcodeproj/xcshareddata/xcschemes/OBPKit-OSX.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /OBPKit.xcodeproj/xcshareddata/xcschemes/OBPKit-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /OBPKit/Connection/OBPServerInfo.h: -------------------------------------------------------------------------------- 1 | // 2 | // OBPServerInfo.h 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 23/01/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | 16 | 17 | @class OBPServerInfo; 18 | typedef NSArray OBPServerInfoArray; 19 | 20 | 21 | 22 | // Keys for values in the dictionary returned by -[OBPServerInfo data] 23 | extern NSString* const OBPServerInfo_APIServer; // e.g. https://apisandbox.openbankproject.com 24 | extern NSString* const OBPServerInfo_APIVersion; // e.g. v1.1 25 | extern NSString* const OBPServerInfo_APIBase; // e.g. https://apisandbox.openbankproject.com/obp/v1.1 26 | extern NSString* const OBPServerInfo_AuthServerBase; // e.g. https://apisandbox.openbankproject.com 27 | extern NSString* const OBPServerInfo_RequestPath; // default: /oauth/initiate 28 | extern NSString* const OBPServerInfo_GetUserAuthPath; // default: /oauth/authorize 29 | extern NSString* const OBPServerInfo_GetTokenPath; // default: /oauth/token 30 | extern NSString* const OBPServerInfo_ClientKey; // (aka Consumer Key) 31 | extern NSString* const OBPServerInfo_ClientSecret; // (aka Consumer Key) 32 | extern NSString* const OBPServerInfo_TokenKey; // (aka AccessToken) 33 | extern NSString* const OBPServerInfo_TokenSecret; // (aka AccessSecret) 34 | 35 | 36 | 37 | /** 38 | OBPServerInfo 39 | 40 | An instance records the data necessary to access an OBP server, storing keys and secrets securely in the key chain. 41 | 42 | The class keeps a persistent record of all complete instances. An instance is complete once its client key and secret have been set. You can get an instance for a new APIServer by asking the class to add an entry for that API server. 43 | 44 | 45 | */ 46 | @interface OBPServerInfo : NSObject 47 | + (nullable instancetype)addEntryForAPIServer:(NSString*)APIServer; ///< add a new instance for accessing the OBP server at url APIServer to the instance recorded by the class. \param APIServer identifies the server through a valid url string, giving scheme, host and optionally the path to the API base including API version path component. \return the new instance. \note If you pass in the API base url, the API server and version will be extracted and set. \note You can have more than one instance for the same server, typically for use with different user logins, and they are differentiated by the unique key property; for user interface, you can differentiate them using the name property. 48 | + (nullable OBPServerInfo*)firstEntryForAPIServer:(NSString*)APIServer; ///< Finds and returns the first entry matching APIServer. \param APIServer identifies the server through a valid url string, giving scheme, host and optionally the path to the API base including API version path component. 49 | + (nullable OBPServerInfo*)defaultEntry; ///< returns the first entry. This will be the oldest instance still held. (Convenience when using only a single entry.) 50 | + (OBPServerInfoArray*)entries; 51 | + (void)removeEntry:(OBPServerInfo*)entry; 52 | 53 | @property (nonatomic, copy, null_resettable) NSString* name; ///< A differentiating name for use in user interface; set to the API host by default 54 | @property (nonatomic, strong, readonly) NSString* APIServer; ///< url string for the API server 55 | @property (nonatomic, strong, readonly) NSString* APIVersion; ///< string for the version of the API to use 56 | @property (nonatomic, strong, readonly) NSString* APIBase; ///< base url for API calls, formed using the APIServer and APIVersion properties 57 | @property (nonatomic, copy) NSDictionary* accessData; ///< Get/set access data. \note When getting data, the returned dictionary contains values for _all_ the OBPServerInfo_ keys defined in OBPServerInfo.h, with derived and default values filled in as necessary. \note When setting data, _only_ the values for the OBPServerInfo_ keys defined in OBPServerInfo.h are copied, while other values held are left unchanged. \note The API host is never changed after the instance has been created, regardless of values passed in for the keys OBPServerInfo_APIServer and OBPServerInfo_APIBase, and the client key and secret are not changeable once set. 58 | @property (nonatomic, copy, nullable) NSDictionary* appData; ///< Get/set general data associated with this server for use by the host app. Persisted. Contents must conform to NSSecureCoding. 59 | @property (readonly) BOOL usable; ///< returns YES when entry has credentials to request access (client key and secret). 60 | @property (readonly) BOOL inUse; ///< returns YES when entry has credentials to access user's data (token key and secret). 61 | @end 62 | /* 63 | Note: Keychain authorisations on OSX during development. 64 | - An OBPServerInfo instance stores credentials in keychain items. 65 | - On iOS the items can only be accessed by the iOS app and no others, and asking the user for permission is never needed. 66 | - On OSX the items can be accessed by trusted applications. 67 | - Each keychain item has an access object containing one or more Access Control Lists, and each ACL contains one or more tags that authorise kinds of operations plus a list of zero or more trusted applications, hence an ACL authorises its trusted applications to perform its tagged operation kinds without further need to request permission from the user. 68 | - The application that creates a keychain item is automatically added as an application that is trusted to read the item, via the item's ACLs. 69 | - However, when you rebuild the application during development, its identifying information (size, dates, content digest, etc.) changes, the trusted application reference in the ACL will not match the new build, and the user is always asked to give permission. This is an inconvenience during development, but does not happen during production, as the app store installer takes care of updating trusted application references. For more information see [Trusted Applications](https://developer.apple.com/library/mac/documentation/Security/Conceptual/keychainServConcepts/02concepts/concepts.html#//apple_ref/doc/uid/TP30000897-CH204-SW5). 70 | */ 71 | 72 | 73 | 74 | /** 75 | OBPServerInfoCustomise allows you to change or replace OBPServerInfo behaviours. 76 | 77 | You can optionally customise any of: instance class, save & load blocks, credential encrypt and decrypt blocks, and/or credential encrypt and decrypt parameters. Just call OBPServerInfoCustomise with a dictionary of customisation information. OBPServerInfoCustomise must be called before +[OBPServerInfo initialize] has been invoked, and once only; other calls are ignored. Values in `config` can be as follows: 78 | 79 | - config[OBPServerInfoConfig_DataClass] gives the subclass of OBPServerInfo to instantiate (OBPServerInfo if absent); 80 | 81 | - config[OBPServerInfoConfig_StoreClass] gives the subclass of OBPServerInfoStore to instantiate (OBPServerInfoStore if absent); 82 | 83 | - config[OBPServerInfoConfig_LoadBlock] and config[OBPServerInfoConfig_SaveBlock] give the blocks to invoke to load entries from and save entries to persistent storage (if less than both are supplied then an OBPServerInfoStore instance is used); 84 | 85 | - config[OBPServerInfoConfig_ClientCredentialEncryptBlock] gives a block to encrypt the client credentials before storage into the keychain; 86 | 87 | - config[OBPServerInfoConfig_ClientCredentialDecryptBlock] gives a block to decrypt the client credentials after retrieval from the keychain; 88 | 89 | - config[OBPServerInfoConfig_ProvideCryptParamsBlock] gives a block to provide parameters for encryption of the client credentials while stored in the keychain; at a minimum, the encryption key and initialisation vector, but a non-default encryption algorithm can also be selected. It is ignored if both a client credential encryption and decryption block is present. Encryption is advisable on OSX because retrieval of the client key and secret via the Key Chain Access application needs just the account login, so another unscrupulous developer could easily download your app and obtain your client key and secret; this problem does not arise on iOS. See also comment describing OBPProvideCryptParamsBlock. 90 | 91 | \note By default OBPServerInfo uses DES encryption (very weak), as DES seems to be the strongest encryption (!) that can gain an exemption from some of the export certification process that is oblogatory for all apps that are distributed through the AppStore. You should consider using stronger encryption in your production app, but you will then also need to get export certification from Apple in order to ship your app — you will need to comply with the requirements for "trade compliance" in iTunes Connect. 92 | */ 93 | void OBPServerInfoCustomise(NSDictionary* config); 94 | 95 | // types for customise config dict values 96 | typedef OBPServerInfoArray*_Nonnull (^OBPServerInfoLoadBlock)(void); 97 | typedef void (^OBPServerInfoSaveBlock)(OBPServerInfoArray* entries); 98 | typedef NSString*_Nonnull (^OBPClientCredentialCryptBlock)(NSString* _Nonnull credential); // (credential) 99 | typedef struct OBPCryptParams { 100 | uint32_t algorithm; uint32_t options; size_t keySize, blockSize; uint8_t *key, *iv; 101 | } OBPCryptParams; 102 | typedef void (^OBPProvideCryptParamsBlock)(OBPCryptParams* ioParams, size_t maxKeySize, size_t maxIVSize); ///< Provide encryption/decryption parameters. When called, *ioParams has working values for all but *key and *iv (initialisation vector). At a minimum, copy your cryptographic key into *(ioParams->key); it is also good practice to copy an initialisation vector into *(ioParams->iv), and this must always be the same length as ioParams->blockSize. Run the GenerateKey target of OBPKit once to generate a file with random byte sequences for use as keys and initialisation vectors, which you can then copy into your project. You can also change the encryption algorithm using values defined in CCAlgorithm in CommonCrypto.h, in which case you must also set compatible key and block sizes as also defined in CommonCrypto.h, and options if appropriate (CCOPtions). Note that anything stronger than DES will require your app to have an export certificate. Parameters maxKeySize and maxIVSize and give the amount of storage pointed to by ioParams->key and ioParams->iv, which should be sufficient for all the algorithm types provided by CommonCrypto.h; if you need more, then point them at your own non-volatile storage. 103 | 104 | // keys for customise confi value type 105 | #define OBPServerInfoConfig_DataClass @"dc" // NSString with class name 106 | #define OBPServerInfoConfig_StoreClass @"sc" // NSString with class name 107 | #define OBPServerInfoConfig_LoadBlock @"lb" // OBPServerInfoLoadBlock 108 | #define OBPServerInfoConfig_SaveBlock @"sb" // OBPServerInfoSaveBlock 109 | #define OBPServerInfoConfig_ClientCredentialEncryptBlock @"eb" // OBPClientCredentialCryptBlock 110 | #define OBPServerInfoConfig_ClientCredentialDecryptBlock @"db" // OBPClientCredentialCryptBlock 111 | #define OBPServerInfoConfig_ProvideCryptParamsBlock @"pp" // OBPProvideCryptParamsBlock 112 | 113 | #define kOBPClientCredentialCryptAlg kCCAlgorithmDES 114 | #define kOBPClientCredentialCryptKeyLen kCCKeySizeDES 115 | #define kOBPClientCredentialCryptIVLen kCCBlockSizeDES 116 | #define kOBPClientCredentialCryptBlockLen kCCBlockSizeDES 117 | // ...see notes above at reason for DES. 118 | 119 | #define kOBPClientCredentialMaxCryptKeyLen kCCKeySizeMaxRC4 120 | #define kOBPClientCredentialMaxCryptBlockLen kCCBlockSizeAES128 121 | // ...limits for buffer sizes 122 | 123 | 124 | 125 | 126 | NS_ASSUME_NONNULL_END 127 | 128 | 129 | -------------------------------------------------------------------------------- /OBPKit/Connection/OBPServerInfo.m: -------------------------------------------------------------------------------- 1 | // 2 | // OBPServerInfo.m 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 23/01/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import "OBPServerInfo.h" 10 | // sdk 11 | #import 12 | #import 13 | // ext 14 | #import 15 | // prj 16 | #import "OBPServerInfoStore.h" 17 | #import "OBPLogging.h" 18 | 19 | 20 | 21 | #define KEY_SEP @"|" 22 | typedef NS_ENUM(uint8_t, EPair) {ePair_ClientKeyAndSecret, ePair_TokenKeyAndSecret}; 23 | #define ClientKeyAndSecretKCAccount @"0" 24 | #define TokenKeyAndSecretKCAccount @"1" 25 | 26 | 27 | 28 | NSString* const OBPServerInfo_APIBase = @"APIBase"; 29 | NSString* const OBPServerInfo_APIServer = @"APIServer"; 30 | NSString* const OBPServerInfo_APIVersion = @"APIVersion"; 31 | NSString* const OBPServerInfo_AuthServerBase = @"AuthServerBase"; 32 | NSString* const OBPServerInfo_RequestPath = @"RequestPath"; 33 | NSString* const OBPServerInfo_GetUserAuthPath = @"GetUserAuthPath"; 34 | NSString* const OBPServerInfo_GetTokenPath = @"GetTokenPath"; 35 | NSString* const OBPServerInfo_ClientKey = @"ClientKey"; // aka Consumer Key 36 | NSString* const OBPServerInfo_ClientSecret = @"ClientSecret"; // aka Consumer Key 37 | NSString* const OBPServerInfo_TokenKey = @"TokenKey"; // aka AccessToken 38 | NSString* const OBPServerInfo_TokenSecret = @"TokenSecret"; // aka AccessSecret 39 | 40 | 41 | 42 | static NSDictionary* gOBPServerInfo = nil; 43 | #define gOBPServerInfoKey_InstanceClass @"class" 44 | #define gOBPServerInfoKey_Store @"store" 45 | #define gOBPServerInfoKey_LoadBlock @"load" 46 | #define gOBPServerInfoKey_SaveBlock @"save" 47 | #define gOBPServerInfoKey_EncryptBlock @"+" 48 | #define gOBPServerInfoKey_DecryptBlock @"-" 49 | #define gOBPServerInfoKey_EntriesArrayHolder @"holder" 50 | 51 | void OBPServerInfoCustomise(NSDictionary* config) 52 | { 53 | if (config == nil || gOBPServerInfo != nil) 54 | return; 55 | 56 | NSMutableDictionary* md = [config mutableCopy]; 57 | Class defClass, useClass, c; 58 | NSString* s; 59 | 60 | // Reject invalid parameters and normalise (while being careful not to trigger +[OBPServerInfo initialize] before we have finished) 61 | 62 | // 1. OBPServerInfo subclass 63 | defClass = objc_getClass("OBPServerInfo"); 64 | s = config[OBPServerInfoConfig_DataClass]; 65 | useClass = s ? objc_getClass([s UTF8String]) : Nil; 66 | for (c = useClass; c != Nil; c = class_getSuperclass(c)) 67 | if (c == defClass) 68 | break; 69 | if (c == Nil) 70 | s = @"OBPServerInfo"; 71 | md[OBPServerInfoConfig_DataClass] = s; 72 | 73 | // 2. OBPServerInfoStore subclass 74 | defClass = objc_getClass("OBPServerInfoStore"); 75 | s = config[OBPServerInfoConfig_StoreClass]; 76 | useClass = s ? objc_getClass([s UTF8String]) : Nil; 77 | for (c = useClass; c != Nil; c = class_getSuperclass(c)) 78 | if (c == defClass) 79 | break; 80 | if (c == Nil) 81 | s = @"OBPServerInfoStore"; 82 | md[OBPServerInfoConfig_StoreClass] = s; 83 | 84 | // 3. Load and Save Block (require both or none) 85 | if (!config[OBPServerInfoConfig_LoadBlock] 86 | ^ !config[OBPServerInfoConfig_SaveBlock]) 87 | { 88 | [md removeObjectForKey: OBPServerInfoConfig_LoadBlock]; 89 | [md removeObjectForKey: OBPServerInfoConfig_SaveBlock]; 90 | } 91 | 92 | // 4. Encrypt & Decrypt blocks (require both or none) 93 | if (!config[OBPServerInfoConfig_ClientCredentialEncryptBlock] 94 | ^ !config[OBPServerInfoConfig_ClientCredentialDecryptBlock]) 95 | { 96 | [md removeObjectForKey: OBPServerInfoConfig_ClientCredentialEncryptBlock]; 97 | [md removeObjectForKey: OBPServerInfoConfig_ClientCredentialDecryptBlock]; 98 | } 99 | 100 | // 5. Only allow encryption param provider if custom enc/decryption not provided 101 | if (config[OBPServerInfoConfig_ClientCredentialEncryptBlock] 102 | && config[OBPServerInfoConfig_ClientCredentialDecryptBlock]) 103 | [md removeObjectForKey: OBPServerInfoConfig_ProvideCryptParamsBlock]; 104 | 105 | gOBPServerInfo = [md copy]; 106 | } 107 | 108 | 109 | 110 | OBPClientCredentialCryptBlock 111 | MakeCryptBlockUsingParamsProvider( 112 | OBPProvideCryptParamsBlock provideParams, 113 | CCOperation op) 114 | { 115 | if (provideParams == nil) 116 | return nil; 117 | 118 | OBPClientCredentialCryptBlock cb = 119 | ^NSString*(NSString* credential) 120 | { 121 | if (![credential length]) 122 | return credential; 123 | 124 | enum { 125 | kKeyStoreMax = kOBPClientCredentialMaxCryptKeyLen * 2, 126 | kIVStoreMax = kOBPClientCredentialMaxCryptBlockLen * 2, 127 | kStackStoreSize = kOBPClientCredentialMaxCryptBlockLen * 8 128 | }; 129 | 130 | uint8_t stackStore[kStackStoreSize]; 131 | uint8_t* buf = stackStore; 132 | size_t bufSize = kStackStoreSize, bufSizeMin; 133 | size_t inputLen, outputLen; 134 | NSString* transformed = credential; 135 | NSData* data; 136 | uint8_t keyStore[kKeyStoreMax]; 137 | uint8_t ivStore[kIVStoreMax]; // initialization vector store 138 | 139 | OBPCryptParams params = { 140 | .algorithm = kOBPClientCredentialCryptAlg, 141 | .options = 0, 142 | .keySize = kOBPClientCredentialCryptKeyLen, 143 | .blockSize = kOBPClientCredentialCryptBlockLen, 144 | .key = keyStore, 145 | .iv = ivStore 146 | }; 147 | 148 | memset(keyStore, 0, kKeyStoreMax); 149 | memset(ivStore, 0, kIVStoreMax); 150 | provideParams(¶ms, kKeyStoreMax, kIVStoreMax); 151 | 152 | if (op == kCCDecrypt) 153 | data = [[NSData alloc] initWithBase64EncodedString: credential options: 0]; 154 | else 155 | data = [credential dataUsingEncoding: NSUTF8StringEncoding]; 156 | inputLen = [data length]; 157 | 158 | bufSizeMin = (inputLen / params.blockSize + 2) * params.blockSize; 159 | if (bufSize < bufSizeMin) 160 | buf = malloc(bufSize = bufSizeMin); 161 | memcpy(buf, [data bytes], inputLen); 162 | memset(buf + inputLen, 0, bufSize - inputLen); 163 | // ...after zeroing remaining buf space, we always need to bump input length up to the 164 | // next multiple of the encryption block size... 165 | inputLen = (inputLen + params.blockSize - 1) / params.blockSize * params.blockSize; 166 | 167 | CCCryptorStatus status = 168 | CCCrypt( 169 | op, // kCCEncrypt or kCCDecrypt 170 | params.algorithm, // AES, DES, RCA, etc. 171 | params.options, // CCOptions: kCCOptionPKCS7Padding, ... 172 | params.key, 173 | params.keySize, 174 | params.iv, // initialization vector, optional 175 | buf, // dataIn, optional per op and alg 176 | inputLen, // dataInLength 177 | buf, // dataOut (can be transformed in place) 178 | bufSize, // dataOutAvailable 179 | &outputLen); // dataOutMoved 180 | 181 | if (outputLen < bufSize) 182 | buf[outputLen] = 0; 183 | 184 | if (status == kCCSuccess) 185 | transformed = op == kCCEncrypt 186 | ? [[NSData dataWithBytes: buf length: outputLen] base64EncodedStringWithOptions: 0] 187 | : [NSString stringWithUTF8String: (char*)buf]; 188 | 189 | if (buf != stackStore) 190 | free(buf); 191 | 192 | return transformed; 193 | }; 194 | return cb; 195 | } 196 | 197 | 198 | 199 | #pragma mark - 200 | @interface OBPServerInfo () 201 | { 202 | NSString* _key; 203 | NSString* _name; 204 | NSString* _APIServer; 205 | NSString* _APIVersion; 206 | NSString* _APIBase; 207 | NSDictionary* _AuthServerDict; 208 | NSDictionary* _cache; 209 | NSDictionary* _appData; 210 | 211 | BOOL _usable; 212 | BOOL _inUse; 213 | } 214 | @property (nonatomic, strong) UICKeyChainStore* keyChainStore; 215 | @end 216 | 217 | 218 | 219 | #pragma mark - 220 | @implementation OBPServerInfo 221 | + (void)initialize 222 | { 223 | if (self != [OBPServerInfo class]) 224 | return; 225 | 226 | // 1. Set up valid class configuration data in gOBPServerInfo, deriving from customisation data if supplied 227 | NSMutableDictionary* md; 228 | NSString* className; 229 | Class class; 230 | OBPServerInfoStore* store; 231 | OBPServerInfoLoadBlock loadBlock; 232 | OBPServerInfoSaveBlock saveBlock; 233 | OBPClientCredentialCryptBlock encryptBlock; 234 | OBPClientCredentialCryptBlock decryptBlock; 235 | OBPProvideCryptParamsBlock provideCryptParams; 236 | 237 | if (gOBPServerInfo == nil) 238 | gOBPServerInfo = @{}; 239 | md = [NSMutableDictionary dictionary]; 240 | 241 | className = gOBPServerInfo[OBPServerInfoConfig_DataClass] ?: @"OBPServerInfo"; 242 | class = NSClassFromString(className); 243 | md[gOBPServerInfoKey_InstanceClass] = class; 244 | 245 | className = gOBPServerInfo[OBPServerInfoConfig_StoreClass] ?: @"OBPServerInfoStore"; 246 | class = NSClassFromString(className); 247 | store = [class alloc]; 248 | store = [store initWithPath: nil]; 249 | md[gOBPServerInfoKey_Store] = store; 250 | 251 | loadBlock = gOBPServerInfo[OBPServerInfoConfig_LoadBlock]; 252 | if (loadBlock == nil) 253 | loadBlock = ^(){return store.entries;}; 254 | md[gOBPServerInfoKey_LoadBlock] = loadBlock; 255 | 256 | saveBlock = gOBPServerInfo[OBPServerInfoConfig_SaveBlock]; 257 | if (saveBlock == nil) 258 | saveBlock = ^(OBPServerInfoArray* entries){store.entries = entries;}; 259 | md[gOBPServerInfoKey_SaveBlock] = saveBlock; 260 | 261 | encryptBlock = gOBPServerInfo[OBPServerInfoConfig_ClientCredentialEncryptBlock]; 262 | decryptBlock = gOBPServerInfo[OBPServerInfoConfig_ClientCredentialDecryptBlock]; 263 | if (!encryptBlock || !decryptBlock) 264 | { 265 | provideCryptParams = gOBPServerInfo[OBPServerInfoConfig_ProvideCryptParamsBlock]; 266 | if (provideCryptParams) 267 | { 268 | encryptBlock = MakeCryptBlockUsingParamsProvider(provideCryptParams, kCCEncrypt); 269 | decryptBlock = MakeCryptBlockUsingParamsProvider(provideCryptParams, kCCDecrypt); 270 | } 271 | else 272 | { 273 | encryptBlock = ^(NSString* s){return s;}; 274 | decryptBlock = ^(NSString* s){return s;}; 275 | } 276 | } 277 | md[gOBPServerInfoKey_EncryptBlock] = encryptBlock; 278 | md[gOBPServerInfoKey_DecryptBlock] = decryptBlock; 279 | 280 | md[gOBPServerInfoKey_EntriesArrayHolder] = [NSMutableArray arrayWithObject: @[]]; 281 | 282 | gOBPServerInfo = [md copy]; 283 | 284 | // 2. Load entries 285 | OBPServerInfoArray* entries = loadBlock(); 286 | NSMutableArray* ma = [NSMutableArray array]; 287 | // Copy valid entries, i.e. keychain still contains corresponding credentials, and remove if not so. (On Mac user can delete keychain items.) 288 | for (OBPServerInfo* entry in entries) 289 | if ([entry checkValid]) 290 | [ma addObject: entry]; 291 | else 292 | OBP_LOG(@"Ignoring invalid entry %@", entry); 293 | gOBPServerInfo[gOBPServerInfoKey_EntriesArrayHolder][0] = [ma copy]; 294 | } 295 | + (OBPServerInfoArray*)entries 296 | { 297 | return gOBPServerInfo[gOBPServerInfoKey_EntriesArrayHolder][0]; 298 | } 299 | + (void)save 300 | { 301 | static BOOL savePending = NO; 302 | if (savePending) 303 | return; 304 | savePending = YES; 305 | dispatch_async(dispatch_get_main_queue(), 306 | ^{ 307 | savePending = NO; 308 | NSMutableArray* ma = [NSMutableArray array]; 309 | for (OBPServerInfo* entry in [self entries]) 310 | if (entry.usable) 311 | [ma addObject:entry]; 312 | OBPServerInfoSaveBlock saveBlock = gOBPServerInfo[gOBPServerInfoKey_SaveBlock]; 313 | saveBlock(ma); 314 | } 315 | ); 316 | } 317 | + (instancetype)defaultEntry 318 | { 319 | OBPServerInfo* entry = [[self entries] firstObject]; 320 | return entry; 321 | } 322 | + (void)removeEntry:(OBPServerInfo*)entry 323 | { 324 | if (!entry) 325 | return; 326 | NSUInteger index = [[self entries] indexOfObjectIdenticalTo: entry]; 327 | if (index != NSNotFound) 328 | { 329 | NSMutableArray* ma = [[self entries] mutableCopy]; 330 | [ma removeObjectIdenticalTo: entry]; 331 | gOBPServerInfo[gOBPServerInfoKey_EntriesArrayHolder][0] = [ma copy]; 332 | [entry storePair: ePair_ClientKeyAndSecret from: @{}]; 333 | [entry storePair: ePair_TokenKeyAndSecret from: @{}]; 334 | entry.keyChainStore = nil; 335 | [self save]; 336 | } 337 | } 338 | + (instancetype)addEntryForAPIServer:(NSString*)APIServer 339 | { 340 | if (![APIServer length]) 341 | return nil; 342 | NSURLComponents* components = [NSURLComponents componentsWithString: APIServer]; 343 | OBP_LOG_IF(nil==components, @"[OBPServerInfo addEntryForAPIServer: %@] - error: not valid as a URL", APIServer); 344 | if (nil==components) 345 | return nil; 346 | NSString* key = [[NSUUID UUID] UUIDString]; 347 | Class class; 348 | OBPServerInfo* entry; 349 | class = gOBPServerInfo[gOBPServerInfoKey_InstanceClass]; 350 | entry = [class alloc]; 351 | entry = [entry initWithKey: key APIServerURLComponents: components]; 352 | gOBPServerInfo[gOBPServerInfoKey_EntriesArrayHolder][0] = [[self entries] arrayByAddingObject: entry]; 353 | // ...save is only scheduled once the entry's data has been assigned and checked 354 | return entry; 355 | } 356 | + (nullable OBPServerInfo*)firstEntryForAPIServer:(NSString*)APIServer 357 | { 358 | if (![APIServer length]) 359 | return nil; 360 | OBPServerInfo* entry; 361 | NSURLComponents* components; 362 | NSString* matchVersion; 363 | NSString* matchServer; 364 | components = [NSURLComponents componentsWithString: APIServer]; 365 | matchVersion = [self versionFromOBPPath: components.path]; 366 | components.path = nil; 367 | matchServer = components.string; 368 | for (entry in [self entries]) 369 | { 370 | if ([matchServer isEqualToString: entry->_APIServer]) 371 | if (!matchVersion || [matchVersion isEqualToString: entry->_APIVersion]) 372 | return entry; 373 | } 374 | return nil; 375 | } 376 | #pragma mark - 377 | + (NSString*)versionFromOBPPath:(NSString*)path 378 | { 379 | if (!path) 380 | return nil; 381 | NSRange rangeOBP; 382 | NSRange rangeEnd; 383 | 384 | if ((rangeOBP = [path rangeOfString: @"obp/v"]).length) 385 | { 386 | path = [path substringFromIndex: rangeOBP.location + rangeOBP.length - 1]; 387 | rangeEnd = [path rangeOfString: @"/"]; 388 | if (rangeEnd.length) 389 | path = [path substringToIndex: rangeEnd.location]; 390 | return path; 391 | } 392 | return nil; 393 | } 394 | + (NSString*)APIBaseForServer:(NSString*)server andAPIVersion:(NSString*)version 395 | { 396 | if (![server length] || ![version length]) 397 | return nil; 398 | NSURLComponents* components; 399 | NSString* base; 400 | components = [NSURLComponents componentsWithString: server]; 401 | components.path = [@"/obp/" stringByAppendingString: version]; 402 | base = components.string; 403 | return base; 404 | } 405 | #pragma mark - 406 | - (instancetype)initWithKey:(NSString*)key APIServerURLComponents:(NSURLComponents*)components 407 | { 408 | if (nil == (self = [super init])) 409 | return nil; 410 | _key = key; 411 | _name = components.host; 412 | _APIVersion = [[self class] versionFromOBPPath: components.path] ?: @"v1.2"; 413 | components.path = nil; 414 | _APIServer = components.string; 415 | _APIBase = [[self class] APIBaseForServer: _APIServer andAPIVersion: _APIVersion]; 416 | _AuthServerDict = @{ 417 | OBPServerInfo_AuthServerBase : _APIServer, 418 | OBPServerInfo_RequestPath : @"/oauth/initiate", 419 | OBPServerInfo_GetUserAuthPath : @"/oauth/authorize", 420 | OBPServerInfo_GetTokenPath : @"/oauth/token", 421 | }; 422 | return self; 423 | } 424 | + (BOOL)supportsSecureCoding 425 | { 426 | return YES; // ==> -initWithCoder: always uses -[NSCoder decodeObjectOfClass:forKey:] 427 | } 428 | - (instancetype)initWithCoder:(NSCoder *)aDecoder 429 | { 430 | if (nil == (self = [super init])) 431 | return nil; 432 | Class classNSString = [NSString class]; 433 | _key = [aDecoder decodeObjectOfClass: classNSString forKey: @"key"]; 434 | _name = [aDecoder decodeObjectOfClass: classNSString forKey: @"name"]; 435 | _APIServer = [aDecoder decodeObjectOfClass: classNSString forKey: @"APIServer"]; 436 | _APIVersion = [aDecoder decodeObjectOfClass: classNSString forKey: @"APIVersion"]; 437 | _APIBase = [aDecoder decodeObjectOfClass: classNSString forKey: @"APIBase"]; 438 | _AuthServerDict = [aDecoder decodeObjectOfClass: [NSDictionary class] forKey: @"AuthServerDict"]; 439 | if ([aDecoder containsValueForKey: @"appData"]) 440 | _appData = [aDecoder decodeObjectOfClass: [NSDictionary class] forKey: @"appData"]; 441 | return self; 442 | } 443 | - (void)encodeWithCoder:(NSCoder *)aCoder 444 | { 445 | [aCoder encodeObject: _key forKey: @"key"]; 446 | [aCoder encodeObject: _name forKey: @"name"]; 447 | [aCoder encodeObject: _APIServer forKey: @"APIServer"]; 448 | [aCoder encodeObject: _APIVersion forKey: @"APIVersion"]; 449 | [aCoder encodeObject: _APIBase forKey: @"APIBase"]; 450 | [aCoder encodeObject: _AuthServerDict forKey: @"AuthServerDict"]; 451 | if (_appData) 452 | [aCoder encodeObject: _appData forKey: @"appData"]; 453 | } 454 | - (void)save 455 | { 456 | if (_usable) 457 | [[self class] save]; 458 | } 459 | #pragma mark - 460 | - (UICKeyChainStore*)keyChainStore 461 | { 462 | if (_keyChainStore == nil) 463 | { 464 | _keyChainStore = [UICKeyChainStore keyChainStoreWithService: _key]; 465 | _keyChainStore.accessibility = UICKeyChainStoreAccessibilityAlways; 466 | } 467 | return _keyChainStore; 468 | } 469 | - (void)fetchPair:(EPair)whichPair into:(NSMutableDictionary*)md 470 | { 471 | BOOL client = whichPair == ePair_ClientKeyAndSecret; 472 | NSString* (^decrypt)(NSString*s) = 473 | client 474 | ? (OBPClientCredentialCryptBlock)gOBPServerInfo[gOBPServerInfoKey_DecryptBlock] 475 | : ^(NSString*s){return s;}; 476 | NSString* key = client ? ClientKeyAndSecretKCAccount : TokenKeyAndSecretKCAccount; 477 | NSString* value; 478 | NSArray* pair; 479 | 480 | if ((_cache && nil != (value = _cache[key])) 481 | || nil != (value = decrypt(self.keyChainStore[key]))) 482 | if (nil != (pair = [value componentsSeparatedByString: KEY_SEP])) 483 | if (2 == [pair count]) 484 | { 485 | md[client ? OBPServerInfo_ClientKey : OBPServerInfo_TokenKey] = pair[0]; 486 | md[client ? OBPServerInfo_ClientSecret : OBPServerInfo_TokenSecret] = pair[1]; 487 | 488 | if (!_cache || ![_cache[key] isEqual: value]) 489 | { 490 | NSMutableDictionary* cache = [(_cache ?: @{}) mutableCopy]; 491 | cache[key] = value; 492 | _cache = [cache copy]; 493 | } 494 | } 495 | } 496 | - (void)storePair:(EPair)whichPair from:(NSDictionary*)d 497 | { 498 | BOOL client = whichPair == ePair_ClientKeyAndSecret; 499 | NSString* (^encrypt)(NSString*s) = 500 | client 501 | ? (OBPClientCredentialCryptBlock)gOBPServerInfo[gOBPServerInfoKey_EncryptBlock] 502 | : ^(NSString*s){return s;}; 503 | NSString* s0 = d[client ? OBPServerInfo_ClientKey : OBPServerInfo_TokenKey]; 504 | NSString* s1 = d[client ? OBPServerInfo_ClientSecret : OBPServerInfo_TokenSecret]; 505 | NSString* key = client ? ClientKeyAndSecretKCAccount : TokenKeyAndSecretKCAccount; 506 | NSString* valueCached = _cache ? _cache[key] : nil; 507 | NSString* value = [s0 length] && [s1 length] 508 | ? [s0 stringByAppendingFormat: @"%@%@", KEY_SEP, s1] 509 | : nil; 510 | 511 | if (!_cache // no cache => update 512 | || !(value ? [valueCached isEqualToString: value] : !valueCached)) // change => update 513 | { 514 | self.keyChainStore[key] = value ? encrypt(value) : nil; 515 | NSMutableDictionary* md = [(_cache ?: @{}) mutableCopy]; 516 | md[key] = value ?: @""; 517 | _cache = [md copy]; 518 | } 519 | 520 | if (client) 521 | _usable = nil != value; 522 | else 523 | _inUse = nil != value; 524 | } 525 | #pragma mark - 526 | - (void)setAccessData:(NSDictionary*)data 527 | { 528 | if (nil == data) 529 | return; 530 | BOOL changed = NO; 531 | BOOL changedToken = NO; 532 | NSString* APIVersion = nil; 533 | NSString *s0, *s1, *k; 534 | 535 | // Check for API version either explicitly or within API base 536 | if ([(s0 = data[OBPServerInfo_APIVersion]) length]) 537 | APIVersion = s0; 538 | else 539 | if ([(s0 = data[OBPServerInfo_APIBase]) length]) 540 | if (![s0 isEqualToString: _APIBase]) 541 | APIVersion = s0.lastPathComponent; 542 | 543 | // Check if API version is changed 544 | if (APIVersion && ![APIVersion isEqualToString: _APIVersion]) 545 | { 546 | _APIVersion = APIVersion; 547 | _APIBase = [[self class] APIBaseForServer: _APIServer andAPIVersion: _APIVersion]; 548 | changed = YES; 549 | } 550 | 551 | // For Auth server base and paths, check for and apply any new non-empty values 552 | NSMutableDictionary* md = [_AuthServerDict mutableCopy]; 553 | for (k in @[OBPServerInfo_AuthServerBase, OBPServerInfo_RequestPath, 554 | OBPServerInfo_GetUserAuthPath, OBPServerInfo_GetTokenPath]) 555 | { 556 | if ([(s0 = data[k]) length]) 557 | if (![(s1 = md[k]) isEqualToString: s0]) 558 | md[k] = s0; 559 | } 560 | if (![_AuthServerDict isEqualToDictionary: md]) 561 | _AuthServerDict = [md copy], changed = YES; 562 | 563 | // If any credentials have been supplied, check and store updates if necessary 564 | if (data[OBPServerInfo_ClientKey] 565 | || data[OBPServerInfo_ClientSecret] 566 | || data[OBPServerInfo_TokenKey] 567 | || data[OBPServerInfo_TokenSecret]) 568 | { 569 | // Fetch current credentials 570 | md = [NSMutableDictionary dictionary]; 571 | [self fetchPair: ePair_ClientKeyAndSecret into: md]; 572 | [self fetchPair: ePair_TokenKeyAndSecret into: md]; 573 | 574 | // Set client key and secret if not already set 575 | int needed = 2; 576 | for (k in @[OBPServerInfo_ClientKey, OBPServerInfo_ClientSecret]) 577 | { 578 | if ([(s0 = data[k]) length]) 579 | if (![(s1 = md[k]) length]) 580 | needed--; 581 | } 582 | if (needed == 0) // we have the key and secret, so save 583 | [self storePair: ePair_ClientKeyAndSecret from: data], _usable = YES, changed = YES; 584 | 585 | // Always update token key and secret, including setting to empty (==logged out or revoked access) 586 | changedToken = NO; 587 | for (k in @[OBPServerInfo_TokenKey, OBPServerInfo_TokenSecret]) 588 | { 589 | if (nil != (s0 = data[k])) 590 | if (![(s1 = md[k]) isEqualToString: s0]) 591 | changedToken = YES; 592 | } 593 | if (changedToken) 594 | [self storePair: ePair_TokenKeyAndSecret from: data]; 595 | 596 | self.keyChainStore = nil; 597 | } 598 | 599 | if (changed) 600 | [self save]; 601 | } 602 | - (NSDictionary*)accessData 603 | { 604 | // load data from key chain and return (never store; we only store retrieval params) 605 | NSMutableDictionary* md = [NSMutableDictionary dictionary]; 606 | md[OBPServerInfo_APIServer] = _APIServer; 607 | md[OBPServerInfo_APIVersion] = _APIVersion; 608 | md[OBPServerInfo_APIBase] = _APIBase; 609 | [md addEntriesFromDictionary: _AuthServerDict]; 610 | [self fetchPair: ePair_ClientKeyAndSecret into: md]; 611 | [self fetchPair: ePair_TokenKeyAndSecret into: md]; 612 | self.keyChainStore = nil; 613 | return [md copy]; 614 | } 615 | - (BOOL)checkValid 616 | { 617 | NSMutableDictionary* md; 618 | md = [NSMutableDictionary dictionary]; 619 | [self fetchPair: ePair_ClientKeyAndSecret into: md]; 620 | [self fetchPair: ePair_TokenKeyAndSecret into: md]; 621 | _usable = [md[OBPServerInfo_ClientKey] length] && [md[OBPServerInfo_ClientSecret] length]; 622 | _inUse = [md[OBPServerInfo_TokenKey] length] && [md[OBPServerInfo_TokenSecret] length]; 623 | return _usable; 624 | } 625 | #pragma mark - 626 | - (void)setAppData:(NSDictionary*)appData 627 | { 628 | if (appData ? [_appData isEqualToDictionary: appData] : !_appData) 629 | return; 630 | _appData = [appData copy]; 631 | [self save]; 632 | } 633 | - (NSDictionary*)appData 634 | { 635 | return _appData; 636 | } 637 | #pragma mark - 638 | - (void)setName:(NSString*)name 639 | { 640 | if (![name length]) 641 | name = [NSURLComponents componentsWithString: _APIServer].host; 642 | if ([name isEqualToString: _name]) 643 | return; 644 | _name = name; 645 | [self save]; 646 | } 647 | @end 648 | 649 | 650 | -------------------------------------------------------------------------------- /OBPKit/Connection/OBPServerInfoStore.h: -------------------------------------------------------------------------------- 1 | // 2 | // OBPServerInfoStore.h 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 15/03/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | 16 | 17 | @class OBPServerInfo; 18 | 19 | 20 | 21 | /// An OBPServerInfoStore instance will handle writing and reading of OBPServerInfo instances to and from persistent storage for the OBPServerInfo class, and hence is a possible override point for implementing your own more secure storage scheme. 22 | @interface OBPServerInfoStore : NSObject 23 | - (instancetype)initWithPath:(nullable NSString*)path; ///< Designated initializer. \param path identifies the location to find/store archived data if non-nil; otherwise a default path is used, which on iOS is file AD.dat in the app's Documents directory, and on OSX is the file AD.dat in ~/Library/Application Support/. 24 | @property (nonatomic, strong, readonly) NSString* path; ///< \returns the path to which data is archived. 25 | @property (nonatomic, copy) NSArray*_Nonnull entries; ///< set synchronously writes the supplied entries to an archive at .path, while get synchronously reads and restores entries from the archive at .path. 26 | @end 27 | 28 | 29 | 30 | NS_ASSUME_NONNULL_END 31 | -------------------------------------------------------------------------------- /OBPKit/Connection/OBPServerInfoStore.m: -------------------------------------------------------------------------------- 1 | // 2 | // OBPServerInfoStore.m 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 15/03/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import "OBPServerInfoStore.h" 10 | #import "OBPServerInfo.h" 11 | 12 | 13 | 14 | @implementation OBPServerInfoStore 15 | { 16 | NSString* _path; 17 | } 18 | - (instancetype)initWithPath:(nullable NSString*)path 19 | { 20 | self = [super init]; 21 | if (self) 22 | _path = path ?: [self defaultPath]; 23 | return self; 24 | } 25 | - (NSString*)defaultPath 26 | { 27 | #define kServerInfoFileName @"SI.dat" 28 | NSString* name; 29 | NSString* path; 30 | name = [NSBundle mainBundle].bundleIdentifier; 31 | #if TARGET_OS_IPHONE 32 | path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; 33 | name = [name stringByAppendingString: @"." kServerInfoFileName]; 34 | #else 35 | path = NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES)[0]; 36 | path = [path stringByAppendingPathComponent: name]; 37 | name = kServerInfoFileName; 38 | [[NSFileManager defaultManager] createDirectoryAtPath: path 39 | withIntermediateDirectories: YES 40 | attributes: nil error: NULL]; 41 | #endif 42 | path = [path stringByAppendingPathComponent: name]; 43 | return path; 44 | } 45 | - (void)setEntries:(OBPServerInfoArray*)entries 46 | { 47 | OBPServerInfoArray* a = [entries copy]; 48 | [NSKeyedArchiver archiveRootObject: a toFile: _path]; 49 | } 50 | - (OBPServerInfoArray*)entries 51 | { 52 | return [NSKeyedUnarchiver unarchiveObjectWithFile: _path] ?: @[]; 53 | } 54 | @end 55 | -------------------------------------------------------------------------------- /OBPKit/Connection/OBPSession.h: -------------------------------------------------------------------------------- 1 | // 2 | // OBPSession.h 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 23/01/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | 16 | 17 | extern NSString* const OBPSessionErrorDomain; 18 | NS_ENUM(NSInteger) { OBPSessionErrorCompletionUnsuccessful = 4096, 19 | OBPSessionErrorCompletionError = 4097, 20 | }; 21 | 22 | 23 | 24 | @class OBPServerInfo; 25 | @class OBPSession; 26 | typedef NSArray OBPSessionArray; 27 | @class OBPWebViewProvider; 28 | @protocol OBPWebViewProvider; 29 | typedef NSObject* OBPWebViewProviderRef; 30 | @class OBPMarshal; 31 | @class STHTTPRequest; 32 | 33 | 34 | 35 | typedef void(^HandleResultBlock)(NSError* _Nullable); 36 | typedef void(^ReceiveDirectLoginParamsBlock)(NSString* _Nullable username, NSString* _Nullable password); // (username, password) 37 | typedef void(^ProvideDirectLoginParamsBlock)(ReceiveDirectLoginParamsBlock receiver); 38 | 39 | 40 | 41 | typedef NS_ENUM(uint8_t, OBPAuthMethod) 42 | { 43 | OBPAuthMethod_None, ///< Don't use authorisation with OBPSession instance. Access is to the public view of public resources only. \note You can alternatively make ad-hoc non-authorised API calls by passing the OBPMarshalOptionOnlyPublicResources option when you use the OBPMarshal object of an authorised OBPSession instance. 44 | OBPAuthMethod_OAuth1, ///< Use OAuth1 authorisation with OBPSession instance. Available on all servers. This is the default auth method. 45 | OBPAuthMethod_DirectLogin, ///< Use direct login with an OBPSession instance. Available on sandbox servers only, for use R&D. Unavailable on production servers. 46 | 47 | OBPAuthMethod_count 48 | }; 49 | 50 | 51 | 52 | typedef NS_ENUM(uint8_t, OBPSessionState) 53 | { 54 | OBPSessionStateInvalid, 55 | OBPSessionStateValid, 56 | OBPSessionStateInvalidating, 57 | OBPSessionStateValidating, 58 | }; 59 | 60 | 61 | 62 | /** OBPSession 63 | 64 | An OBPSession *instance* sets up authorised sessions with an OBP server, can authorise subsequent API requests, and provides convenient access to a helper for marshalling resources through the API. 65 | 66 | The OBPSession *class* object holds and manages instances, allowing you to find, add, remove and gain general access to them. 67 | 68 | 69 | An OBPSession instance associates with one OBPServerInfo through its lifetime, which represents the server to connect to and persists credentials. Hence, if the serverInfo has restored authorisation from a previous run of the host application, then a session is ready to continue immediately the instance is created. 70 | 71 | A session instance will need to present a web view to gain authorisation from the user when using OBPAuthMethod_OAuth1. You can optionally provide this by introducing an object that implements the OBPWebViewProvider protocol. Otherwise, the OBPDefaultWebViewProvider singleton will be used. 72 | 73 | You can also use authorisation method OBPAuthMethod_DirectLogin during development. 74 | 75 | If only access to public resources is required, use authorisation method OBPAuthMethod_None. 76 | 77 | Each instance holds an OBPMarshal helper object in its marshal property, for retrieving resources through the API. You can replace the default instance with your own if you need a different implementation or behaviour. 78 | */ 79 | @interface OBPSession : NSObject 80 | // Managing sessions 81 | + (nullable instancetype)sessionWithServerInfo:(OBPServerInfo*)serverInfo; ///< Return a session instance for use with the server represented by the supplied info, creating it if necessary; there is only one OBPSession instance per OBPServerInfo instance. \param serverInfo identifies the OBP API server to connect to, and is retained by the OBPSession instance for its lifetime. 82 | + (nullable OBPSession*)findSessionWithServerInfo:(OBPServerInfo*)serverInfo; ///< Return the OBPSession instance that uses the supplied OBPServerInfo instance, or nil if not found. 83 | + (void)removeSession:(OBPSession*)session; ///< Remove the supplied instance from the class' record of all sessions. 84 | + (nullable OBPSession*)currentSession; ///< Return the first session instance. (Convenience for when working with a single session at a time.) 85 | + (void)setCurrentSession:(OBPSession*)session; ///< Set session to be the current session. (Moves existing session to front of all sessions.) 86 | + (OBPSessionArray*)allSessions; ///< Return the list of session instances currently held by the class. 87 | 88 | // Instance set up 89 | @property (nonatomic, weak, nullable) OBPWebViewProviderRef webViewProvider; ///< Set/get an object that can provide a web view when requested by this instance, i.e. conforming to protocol OBPWebViewProvider. The web view is for use during the session validation process to ask the user to authorise access to their data. If nil, then the OBPDefaultWebViewProvider singleton is used. Changes to the web view provider are ignored while validation is in progress. 90 | 91 | @property (nonatomic, strong, readonly) OBPServerInfo* serverInfo; ///< Get the OBPServerInfo instance that represents the OBP API server with which this session connects. 92 | 93 | @property (nonatomic, assign) OBPAuthMethod authMethod; ///< Get and set the method used to authorize calls in this session. The default is OBPAuthMethod_OAuth1. Changing the auth method will invalidate the current session. \note An alternative to using OBPAuthMethod_None (access to public view of publicy available resources), is to use authorisation with this instance, and then on as-needed basis, make ad-hoc non-authorised API calls by passing OBPMarshalOptionOnlyPublicResources with your use of the OBPMarshal object of this instance. 94 | @property (nonatomic, strong) ProvideDirectLoginParamsBlock directLoginParamsProvider; ///< Optionally set a parameter provider for use with direct login. It will be invoked when validating while authMethod is OBPAuthMethod_DirectLogin; the provider is passed a receiver block which it must call with the username and password once it has obtained them, or nil values if unsuccessful. If no provider has been set then username and password are requested using UIAlertController. 95 | 96 | 97 | // Connection 98 | - (void)validate:(HandleResultBlock)completion; ///< Ask this session to go through the authorisation process and call the supplied completion block with the result. If the session is already valid, then the request is ignored. If it is currently validating, then the previous validation attempt is aborted, completion called with a NSUserCancelledError, and a new validation sequence started. A strong reference to the webViewProvider (or if nil, to -[OBPDefaultWebViewProvider instance]) is taken, held until completion and used to bring up a web view when the user is asked for authorisation. If validation is completed successfully, then authorisation is stored through the serverInfo instance, and completion is called with nil for error. If validation fails, completion is called with an error. If user authorisation is sought via an external browser, it is possible that the user does not complete it (e.g. browsed away from auth web page), and no completion is returned, and it is for this scenario that subsequent calls to validate will abandon the original request and start a fresh one. 99 | 100 | - (void)invalidate; ///< Make the session invalid. Any persisted authorisation is discarded, and therefore new validation is needed before any more interaction with the OBP server is possible. If a validation was in progress, it is canceled and its completion called with NSUserCancelledError. 101 | 102 | @property (nonatomic, readonly) OBPSessionState state; ///< Find out the current OBPSessionState for this instance, i.e. whether this session is valid, in the process of validating or invalidating, or is invalid. 103 | 104 | @property (nonatomic, readonly) BOOL valid; ///< Find out whether this instance has state OBPSessionStateValid. This property is observable using KVO; change notifications happen after validation completion callbacks have been called. 105 | 106 | // Authorising subsequent requests in a valid session 107 | - (BOOL)authorizeSTHTTPRequest:(STHTTPRequest*)request; ///< Add an authorisation header to the supplied request. Call this as the very last step before launching the request. 108 | - (BOOL)authorizeURLRequest:(NSMutableURLRequest*)request andWrapErrorHandler:(HandleResultBlock _Nullable * _Nonnull)errorHandlerAt; ///< Add an authorisation header to the supplied request. Call as last step before launching an NSURLRequest. \param errorHandlerAt points to your local variable that references an error handler block which you will use to handle any errors from the execution of request; it will be replaced by an error handler belonging to this instance, and which will in turn call your original handler; this is necessary so that this instance can detect any errors that show access has been revoked. 109 | 110 | // Access helper for marshalling resources through OBP API as part of this session 111 | @property (nonatomic, strong) OBPMarshal* marshal; ///< Get a default helper for marshalling resources through the OBP API (or one that has been previously assigned) for this session. Reseting to nil will cause a default marshal helper to be created at next request. Assign your own subclass instance if you need an alternative implementation to be used. 112 | @end 113 | 114 | 115 | 116 | NS_ASSUME_NONNULL_END 117 | -------------------------------------------------------------------------------- /OBPKit/Connection/OBPSession.m: -------------------------------------------------------------------------------- 1 | // 2 | // OBPSession.m 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 23/01/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import "OBPSession.h" 10 | // sdk 11 | #if TARGET_OS_IPHONE 12 | #import 13 | #else 14 | #import 15 | #endif 16 | // ext 17 | #import 18 | #import 19 | // prj 20 | #import "OBPLogging.h" 21 | #import "OBPServerInfo.h" 22 | #import "OBPWebViewProvider.h" 23 | #import "OBPMarshal.h" 24 | #import "NSString+OBPKit.h" 25 | #import "STHTTPRequest+Error.h" 26 | 27 | 28 | 29 | NSString* const OBPSessionErrorDomain = @"OBPSession"; 30 | 31 | #define DL_TOKEN_SECRET @"-" 32 | 33 | 34 | 35 | @interface OBPSession () 36 | { 37 | OBPServerInfo* _serverInfo; 38 | OBPMarshal* _marshal; 39 | // 40 | OBPAuthMethod _authMethod; 41 | // Direct Login 42 | // OAuth1 43 | OBPWebViewProviderRef _WVProvider; 44 | NSString* _callbackURLString; 45 | NSString* _requestToken; 46 | NSString* _requestTokenSecret; 47 | NSString* _verifier; 48 | } 49 | @property (nonatomic, readwrite) OBPSessionState state; 50 | @property (nonatomic, strong) HandleResultBlock validateCompletion; 51 | @property (nonatomic, readwrite) BOOL valid; 52 | @end 53 | 54 | 55 | 56 | @interface OBPSession (OAuth1) 57 | - (void)startValidating1; 58 | - (void)addAuthorizationHeader1ToSTHTTPRequest:(STHTTPRequest*)request; 59 | - (void)addAuthorizationHeader1ToURLRequest:(NSMutableURLRequest*)request; 60 | @end 61 | 62 | 63 | 64 | @interface OBPSession (DirectLogin) 65 | - (void)startValidating2; 66 | - (void)addAuthorizationHeader2ToSTHTTPRequest:(STHTTPRequest*)request; 67 | - (void)addAuthorizationHeader2ToURLRequest:(NSMutableURLRequest*)request; 68 | @end 69 | 70 | 71 | 72 | #pragma mark - 73 | @implementation OBPSession 74 | static OBPSessionArray* sSessions = nil; 75 | + (void)initialize 76 | { 77 | if (self != [OBPSession class]) 78 | return; 79 | sSessions = [OBPSessionArray array]; 80 | } 81 | + (nullable OBPSession*)currentSession 82 | { 83 | return [sSessions firstObject]; 84 | } 85 | + (void)setCurrentSession:(OBPSession*)session 86 | { 87 | NSUInteger index = session ? [sSessions indexOfObjectIdenticalTo: session] : NSNotFound; 88 | OBP_LOG_IF(index == NSNotFound, @"[OBPSession setCurrentSession: %@] — bad parameter.", session); 89 | if (index == NSNotFound || index == 0) 90 | return; 91 | NSMutableArray* ma = [sSessions mutableCopy]; 92 | [ma removeObjectIdenticalTo: session]; 93 | [ma insertObject: session atIndex: 0]; 94 | sSessions = [ma copy]; 95 | } 96 | + (nullable instancetype)findSessionWithServerInfo:(OBPServerInfo*)serverInfo 97 | { 98 | OBPSession* session; 99 | for (session in sSessions) 100 | if (session->_serverInfo == serverInfo) 101 | return session; 102 | return nil; 103 | } 104 | + (nullable instancetype)sessionWithServerInfo:(OBPServerInfo*)serverInfo 105 | { 106 | OBPSession* session; 107 | if (!serverInfo) 108 | return nil; 109 | if (nil != (session = [self findSessionWithServerInfo: serverInfo])) 110 | return session; 111 | session = [[self alloc] initWithServerInfo: serverInfo]; 112 | session.webViewProvider = [OBPDefaultWebViewProvider instance]; 113 | sSessions = [sSessions arrayByAddingObject: session]; 114 | return session; 115 | } 116 | + (void)removeSession:(OBPSession*)session 117 | { 118 | if (NSNotFound == [sSessions indexOfObjectIdenticalTo: session]) 119 | return; 120 | if (session.state != OBPSessionStateInvalid) 121 | [session invalidate]; 122 | NSMutableArray* ma = [sSessions mutableCopy]; 123 | [ma removeObjectIdenticalTo: session]; 124 | sSessions = [ma copy]; 125 | } 126 | + (OBPSessionArray*)allSessions 127 | { 128 | return [sSessions copy]; 129 | } 130 | #pragma mark - 131 | - (instancetype)initWithServerInfo:(OBPServerInfo*)serverInfo 132 | { 133 | if (!serverInfo) 134 | self = nil; 135 | else 136 | self = [super init]; 137 | 138 | if (self) 139 | { 140 | _serverInfo = serverInfo; 141 | NSDictionary* d = _serverInfo.accessData; 142 | NSString* token = d[OBPServerInfo_TokenKey]; 143 | NSString* secret = d[OBPServerInfo_TokenSecret]; 144 | if (0 != [token length] * [secret length]) 145 | _valid = YES, _state = OBPSessionStateValid; 146 | _authMethod = OBPAuthMethod_OAuth1; 147 | if ([DL_TOKEN_SECRET isEqualToString: secret]) 148 | _authMethod = OBPAuthMethod_DirectLogin; 149 | } 150 | 151 | return self; 152 | } 153 | #pragma mark - 154 | - (void)setMarshal:(OBPMarshal*)marshal 155 | { 156 | if (marshal && marshal.session != self) 157 | marshal = nil; 158 | if (_marshal != marshal) 159 | _marshal = marshal; 160 | } 161 | - (OBPMarshal*)marshal 162 | { 163 | if (_marshal == nil) 164 | _marshal = [[OBPMarshal alloc] initWithSessionAuth: self]; 165 | return _marshal; 166 | } 167 | #pragma mark - 168 | - (void)setAuthMethod:(OBPAuthMethod)authMethod 169 | { 170 | if (_authMethod != authMethod) 171 | { 172 | [self invalidate]; 173 | _authMethod = authMethod; 174 | } 175 | } 176 | #pragma mark - 177 | - (void)validate:(HandleResultBlock)completion 178 | { 179 | OBP_ASSERT(_state == OBPSessionStateInvalid); 180 | if (_state != OBPSessionStateValid) 181 | if (completion) 182 | { 183 | if (_state == OBPSessionStateValidating) 184 | [self invalidate]; 185 | _validateCompletion = completion; 186 | [self startValidating]; 187 | } 188 | } 189 | - (void)invalidate 190 | { 191 | NSDictionary* data = @{ 192 | OBPServerInfo_TokenKey : @"", 193 | OBPServerInfo_TokenSecret : @"", 194 | }; 195 | [self endWithState: OBPSessionStateInvalid data: data error: nil]; 196 | } 197 | - (void)startValidating 198 | { 199 | switch (_authMethod) 200 | { 201 | case OBPAuthMethod_None: 202 | [self endWithState: OBPSessionStateValid data: nil error: nil]; 203 | break; 204 | case OBPAuthMethod_OAuth1: 205 | [self startValidating1]; 206 | break; 207 | case OBPAuthMethod_DirectLogin: 208 | [self startValidating2]; 209 | break; 210 | default: 211 | break; 212 | } 213 | } 214 | - (void)endWithState:(OBPSessionState)newState data:(NSDictionary*)data error:(NSError*)error // Vagueness of object message name is deliberate. 215 | { 216 | BOOL validNow = newState == OBPSessionStateValid; 217 | BOOL validWas = _valid; 218 | _state = newState; 219 | _serverInfo.accessData = data; 220 | if (_validateCompletion) 221 | { 222 | // We want to call the completion function before KV observers of our valid property get notified. 223 | HandleResultBlock validateCompletion = _validateCompletion; 224 | _validateCompletion = nil; 225 | _valid = validNow; // temporarily set _valid to correct value without triggering KVO 226 | if (error == nil && !validNow) 227 | error = [NSError errorWithDomain: NSCocoaErrorDomain code: NSUserCancelledError userInfo: nil]; 228 | validateCompletion(error); 229 | _valid = validWas; // temporarily reset _valid to old value so KVO will be triggered next... 230 | } 231 | self.valid = validNow; 232 | } 233 | #pragma mark - 234 | - (HandleResultBlock)detectRevokeBlockWithChainToBlock:(HandleResultBlock)chainBlock 235 | { 236 | HandleResultBlock block = 237 | ^(NSError* error) 238 | { 239 | switch (error.code) 240 | { 241 | case 401: 242 | OBP_LOG(@"Request got 401 Unauthorized => Access to server %@ revoked", self.serverInfo.name); 243 | [self invalidate]; 244 | break; 245 | 246 | case NSURLErrorUserAuthenticationRequired: 247 | if (![error.domain isEqualToString: NSURLErrorDomain]) 248 | break; 249 | OBP_LOG(@"Request got NSURLErrorUserAuthenticationRequired => Access to server %@ revoked", self.serverInfo.name); 250 | [self invalidate]; 251 | break; 252 | } 253 | 254 | if (chainBlock != nil) 255 | chainBlock(error); 256 | }; 257 | 258 | return block; 259 | } 260 | - (BOOL)addAuthorizationHeaderToSTHTTPRequest:(STHTTPRequest*)request 261 | { 262 | switch (_authMethod) 263 | { 264 | case OBPAuthMethod_OAuth1: 265 | [self addAuthorizationHeader1ToSTHTTPRequest: request]; 266 | break; 267 | case OBPAuthMethod_DirectLogin: 268 | [self addAuthorizationHeader2ToSTHTTPRequest: request]; 269 | break; 270 | default: 271 | return NO; 272 | break; 273 | } 274 | return YES; 275 | } 276 | - (BOOL)addAuthorizationHeaderToURLRequest:(NSMutableURLRequest*)request 277 | { 278 | switch (_authMethod) 279 | { 280 | case OBPAuthMethod_OAuth1: 281 | [self addAuthorizationHeader1ToURLRequest: request]; 282 | break; 283 | case OBPAuthMethod_DirectLogin: 284 | [self addAuthorizationHeader2ToURLRequest: request]; 285 | break; 286 | default: 287 | return NO; 288 | break; 289 | } 290 | return YES; 291 | } 292 | - (BOOL)authorizeSTHTTPRequest:(STHTTPRequest*)request 293 | { 294 | OBP_ASSERT(self.state == OBPSessionStateValid || self.authMethod == OBPAuthMethod_None); 295 | if (self.state != OBPSessionStateValid && self.authMethod != OBPAuthMethod_None) 296 | return NO; 297 | 298 | HandleResultBlock errorBlock = request.errorBlock; 299 | 300 | // If auth header installed, chain error handler to check if token has been revoked 301 | if ([self addAuthorizationHeaderToSTHTTPRequest: request]) 302 | errorBlock = [self detectRevokeBlockWithChainToBlock: errorBlock]; 303 | 304 | if (errorBlock) 305 | { 306 | STHTTPRequest __weak* request_ifStillAround = request; 307 | errorBlock = 308 | ^(NSError* error) 309 | { 310 | STHTTPRequest* request = request_ifStillAround; 311 | // Add server-side description to error if available. (STHTTPRequest enhancement) 312 | error = request ? [request errorByAddingServerSideDescriptionToError: error] : error; 313 | errorBlock(error); 314 | }; 315 | } 316 | 317 | request.errorBlock = errorBlock; 318 | 319 | return YES; 320 | } 321 | - (BOOL)authorizeURLRequest:(NSMutableURLRequest*)request andWrapErrorHandler:(HandleResultBlock*)handlerAt 322 | { 323 | OBP_ASSERT(self.state == OBPSessionStateValid || self.authMethod == OBPAuthMethod_None); 324 | if (self.state != OBPSessionStateValid && self.authMethod != OBPAuthMethod_None) 325 | return NO; 326 | 327 | // If auth header installed, chain error handler to check if token has been revoked 328 | if ([self addAuthorizationHeaderToURLRequest: request]) 329 | if (handlerAt) 330 | *handlerAt = [self detectRevokeBlockWithChainToBlock: *handlerAt]; 331 | 332 | return YES; 333 | } 334 | @end 335 | 336 | 337 | 338 | #pragma mark - 339 | @implementation OBPSession (OAuth1) 340 | - (void)startValidating1 341 | { 342 | // We keep a strong reference to the webViewProvider for the duration of our authentication process... 343 | _WVProvider = _webViewProvider ?: [OBPDefaultWebViewProvider instance]; 344 | [self getAuthRequestToken]; 345 | } 346 | #pragma mark - 347 | - (void)completedWith:(NSString*)token and:(NSString*)secret error:(NSError*)error // Vagueness of object message name is deliberate. 348 | { 349 | [_WVProvider resetWebViewProvider]; 350 | _WVProvider = nil; 351 | _callbackURLString = nil; 352 | _requestToken = nil; 353 | _requestTokenSecret = nil; 354 | _verifier = nil; 355 | 356 | OBPSessionState newState; 357 | if ([token length] && [secret length]) 358 | newState = OBPSessionStateValid; 359 | else 360 | newState = OBPSessionStateInvalid, token = secret = @""; 361 | 362 | NSDictionary* data = @{ 363 | OBPServerInfo_TokenKey : token, 364 | OBPServerInfo_TokenSecret : secret, 365 | }; 366 | 367 | [self endWithState: newState data: data error: error]; 368 | } 369 | - (void)getAuthRequestToken 370 | { 371 | NSDictionary* d = _serverInfo.accessData; 372 | NSString* base = d[OBPServerInfo_AuthServerBase]; 373 | NSString* path = d[OBPServerInfo_RequestPath]; 374 | NSString* consumerKey = d[OBPServerInfo_ClientKey]; 375 | NSString* consumerSecret = d[OBPServerInfo_ClientSecret]; 376 | NSString* callbackScheme; 377 | NSString* header; 378 | STHTTPRequest* request; 379 | STHTTPRequest __weak* request_ifStillAround; 380 | 381 | path = [base stringForURLByAppendingPath: path]; 382 | request_ifStillAround = request = [STHTTPRequest requestWithURLString: path]; 383 | request.HTTPMethod = @"POST"; 384 | request.POSTDictionary = @{}; 385 | 386 | callbackScheme = _WVProvider.callbackScheme; 387 | if (![callbackScheme length]) 388 | callbackScheme = [NSBundle mainBundle].bundleIdentifier; 389 | _callbackURLString = [callbackScheme stringByAppendingString: @"://callback"]; 390 | 391 | header = OAuthHeader( 392 | request.url, 393 | request.HTTPMethod, 394 | nil, // body (parsed for extra query parameters) 395 | consumerKey, 396 | consumerSecret, 397 | nil, // oauth_token 398 | nil, // oauth_token_secret 399 | nil, // oauth_verifier 400 | _callbackURLString, 401 | OAuthCoreSignatureMethod_HMAC_SHA256); 402 | 403 | [request setHeaderWithName: @"Authorization" value: header]; 404 | 405 | request.completionBlock = 406 | ^(NSDictionary *headers, NSString *body) 407 | { 408 | STHTTPRequest* request = request_ifStillAround; 409 | NSInteger status = request.responseStatus; 410 | NSDictionary* response; 411 | NSString* callbackResult; 412 | BOOL completedStage = NO; 413 | 414 | if (status == 200) 415 | { 416 | body = [body stringByRemovingPercentEncoding]; 417 | response = [body extractURLQueryParams]; 418 | callbackResult = response[@"oauth_callback_confirmed"]; 419 | if([callbackResult isEqualToString: @"true"]) 420 | { 421 | self->_requestToken = response[@"oauth_token"]; 422 | self->_requestTokenSecret = response[@"oauth_token_secret"]; 423 | [self getUsersAuthorisation]; 424 | completedStage = YES; 425 | } 426 | } 427 | 428 | if (!completedStage) 429 | { 430 | OBP_LOG(@"getAuthRequestToken request completion not successful: status=%d headers=%@ body=%@", (int)status, headers, body); 431 | [self completedWith: nil and: nil error: [NSError errorWithDomain: OBPSessionErrorDomain code: OBPSessionErrorCompletionUnsuccessful userInfo: @{@"status":@(status), NSURLErrorKey:request?request.url:[NSNull null]}]]; 432 | } 433 | }; 434 | 435 | request.errorBlock = 436 | ^(NSError *error) 437 | { 438 | STHTTPRequest *request = request_ifStillAround; 439 | // Add server-side description to error if available. (STHTTPRequest enhancement) 440 | error = request ? [request errorByAddingServerSideDescriptionToError: error] : error; 441 | OBP_LOG(@"getAuthRequestToken got error %@", error); 442 | [self completedWith: nil and: nil error: [NSError errorWithDomain: OBPSessionErrorDomain code: OBPSessionErrorCompletionError userInfo: @{NSUnderlyingErrorKey:error,NSURLErrorKey:request_ifStillAround.url?:[NSNull null]}]]; 443 | }; 444 | 445 | _state = OBPSessionStateValidating; 446 | [request startAsynchronous]; 447 | self.valid = _state == OBPSessionStateValid; 448 | } 449 | - (void)getUsersAuthorisation 450 | { 451 | NSDictionary* d = _serverInfo.accessData; 452 | NSString* base = d[OBPServerInfo_AuthServerBase]; 453 | NSString* path = d[OBPServerInfo_GetUserAuthPath]; 454 | NSURLComponents* baseComponents = [NSURLComponents componentsWithString: base]; 455 | NSURL* url; 456 | 457 | baseComponents.path = path; 458 | baseComponents.queryItems = @[[NSURLQueryItem queryItemWithName: @"oauth_token" value: _requestToken]]; 459 | url = baseComponents.URL; // returns nil if path not prefixed by "/" 460 | OBP_ASSERT(url); 461 | 462 | OBPWebNavigationFilter filter = 463 | ^BOOL(NSURL* url) 464 | { 465 | NSDictionary* parameters; 466 | NSString* requestToken; 467 | NSString* urlString = [url absoluteString]; 468 | 469 | if ([urlString hasPrefix: self->_callbackURLString]) 470 | if (nil != (parameters = [url.query extractURLQueryParams])) 471 | if (nil != (requestToken = parameters[@"oauth_token"])) 472 | if ([self->_requestToken isEqualToString: requestToken]) 473 | { 474 | self->_verifier = parameters[@"oauth_verifier"]; 475 | [self getAccessToken]; 476 | return YES; 477 | } 478 | 479 | return NO; 480 | }; 481 | 482 | OBPWebCancelNotifier cancel = 483 | ^() 484 | { 485 | [self completedWith: nil and: nil 486 | error: [NSError errorWithDomain: NSCocoaErrorDomain 487 | code: NSUserCancelledError 488 | userInfo: @{ NSURLErrorKey : url } ]]; 489 | }; 490 | 491 | [_WVProvider showURL: url filterNavWith: filter notifyCancelBy: cancel]; 492 | } 493 | - (void)getAccessToken 494 | { 495 | NSDictionary* d = _serverInfo.accessData; 496 | NSString* base = d[OBPServerInfo_AuthServerBase]; 497 | NSString* path = d[OBPServerInfo_GetTokenPath]; 498 | NSString* consumerKey = d[OBPServerInfo_ClientKey]; 499 | NSString* consumerSecret = d[OBPServerInfo_ClientSecret]; 500 | NSString* header; 501 | STHTTPRequest* request; 502 | STHTTPRequest __weak* request_ifStillAround; 503 | 504 | path = [base stringForURLByAppendingPath: path]; 505 | request_ifStillAround = request = [STHTTPRequest requestWithURLString: path]; 506 | request.HTTPMethod = @"POST"; 507 | request.POSTDictionary = @{}; 508 | 509 | header = OAuthHeader( 510 | request.url, 511 | request.HTTPMethod, 512 | nil, // body (parsed for extra query parameters) 513 | consumerKey, 514 | consumerSecret, 515 | _requestToken, // oauth_token 516 | _requestTokenSecret, // oauth_token_secret 517 | _verifier, // oauth_verifier, 518 | _callbackURLString, 519 | OAuthCoreSignatureMethod_HMAC_SHA256); 520 | 521 | [request setHeaderWithName: @"Authorization" value: header]; 522 | 523 | request.completionBlock = 524 | ^(NSDictionary *headers, NSString *body) 525 | { 526 | STHTTPRequest* request = request_ifStillAround; 527 | NSInteger status = request.responseStatus; 528 | NSDictionary* response; 529 | NSString* token; 530 | NSString* secret; 531 | BOOL completedStage = NO; 532 | 533 | if (status == 200) 534 | { 535 | body = [body stringByRemovingPercentEncoding]; 536 | response = [body extractURLQueryParams]; 537 | token = response[@"oauth_token"]; 538 | secret = response[@"oauth_token_secret"]; 539 | [self completedWith: token and: secret error: nil]; 540 | completedStage = YES; 541 | } 542 | 543 | if (!completedStage) 544 | { 545 | OBP_LOG(@"getAccessToken request completion not successful: status=%d headers=%@ body=%@", (int)status, headers, body); 546 | [self completedWith: nil and: nil error: [NSError errorWithDomain: OBPSessionErrorDomain code: OBPSessionErrorCompletionUnsuccessful userInfo: @{@"status":@(status), NSURLErrorKey:request.url?:[NSNull null]}]]; 547 | } 548 | }; 549 | 550 | request.errorBlock = 551 | ^(NSError *error) 552 | { 553 | STHTTPRequest *request = request_ifStillAround; 554 | // Add server-side description to error if available. (STHTTPRequest enhancement) 555 | error = request ? [request errorByAddingServerSideDescriptionToError: error] : error; 556 | OBP_LOG(@"getAccessToken got error %@", error); 557 | [self completedWith: nil and: nil error: [NSError errorWithDomain: OBPSessionErrorDomain code: OBPSessionErrorCompletionError userInfo: @{NSUnderlyingErrorKey:error,NSURLErrorKey:request_ifStillAround.url?:[NSNull null]}]]; 558 | }; 559 | 560 | [request startAsynchronous]; 561 | } 562 | #pragma mark - 563 | - (void)addAuthorizationHeader1ToSTHTTPRequest:(STHTTPRequest*)request 564 | { 565 | NSDictionary* d = _serverInfo.accessData; 566 | NSString* consumerKey = d[OBPServerInfo_ClientKey]; 567 | NSString* consumerSecret = d[OBPServerInfo_ClientSecret]; 568 | NSString* tokenKey = d[OBPServerInfo_TokenKey]; 569 | NSString* tokenSecret = d[OBPServerInfo_TokenSecret]; 570 | NSString* header; 571 | 572 | OBP_ASSERT(0 != [consumerKey length] * [consumerSecret length] * [tokenKey length] * [tokenSecret length] && ![DL_TOKEN_SECRET isEqualToString: tokenSecret]); 573 | // If STHTTPRequest's HTTPMethod property is not explicitly set, then STHTTPRequest infers it lazily at the last moment, and we can get a value from the property that is not yet accurate at this stage. Assert that this is not the case here: 574 | OBP_ASSERT(([request.HTTPMethod isEqualToString: @"GET"] || [request.HTTPMethod isEqualToString: @"DELETE"]) == (request.POSTDictionary==nil && request.rawPOSTData==nil)); 575 | 576 | header = OAuthHeader( 577 | request.url, 578 | request.HTTPMethod, 579 | nil, // body (parsed for extra query parameters) 580 | consumerKey, 581 | consumerSecret, 582 | tokenKey, // oauth_token 583 | tokenSecret, // oauth_token_secret 584 | nil, // oauth_verifier, 585 | nil, // callback 586 | OAuthCoreSignatureMethod_HMAC_SHA256); 587 | 588 | [request setHeaderWithName: @"Authorization" value: header]; 589 | } 590 | - (void)addAuthorizationHeader1ToURLRequest:(NSMutableURLRequest*)request 591 | { 592 | NSDictionary* d = _serverInfo.accessData; 593 | NSString* consumerKey = d[OBPServerInfo_ClientKey]; 594 | NSString* consumerSecret = d[OBPServerInfo_ClientSecret]; 595 | NSString* tokenKey = d[OBPServerInfo_TokenKey]; 596 | NSString* tokenSecret = d[OBPServerInfo_TokenSecret]; 597 | NSString* header; 598 | 599 | OBP_ASSERT(0 != [consumerKey length] * [consumerSecret length] * [tokenKey length] * [tokenSecret length] && ![DL_TOKEN_SECRET isEqualToString: tokenSecret]); 600 | 601 | header = OAuthHeader( 602 | request.URL, 603 | request.HTTPMethod, 604 | nil, // body (parsed for extra query parameters) 605 | consumerKey, 606 | consumerSecret, 607 | tokenKey, // oauth_token 608 | tokenSecret, // oauth_token_secret 609 | nil, // oauth_verifier 610 | nil, // callback 611 | OAuthCoreSignatureMethod_HMAC_SHA256); 612 | 613 | [request setValue: header forHTTPHeaderField: @"Authorization"]; 614 | } 615 | @end 616 | 617 | 618 | 619 | #pragma mark - 620 | @implementation OBPSession (DirectLogin) 621 | - (void)startValidating2 622 | { 623 | ProvideDirectLoginParamsBlock provider = 624 | _directLoginParamsProvider 625 | ?: 626 | ^void(ReceiveDirectLoginParamsBlock receiver) 627 | { 628 | NSString* title = @"Log In"; 629 | NSString* message = [NSString stringWithFormat: @"Please enter your Username and Password to log in to\n%@", self.serverInfo.name]; 630 | NSString* actionButtonTitle = @"Log In"; 631 | NSString* cancelButtonTitle = @"Cancel"; 632 | NSString* usernamePlaceholder = @"Username"; 633 | NSString* passwordPlaceholder = @"Password"; 634 | #if TARGET_OS_IPHONE 635 | UIAlertController* ac; 636 | UIAlertAction* aa; 637 | ac = [UIAlertController alertControllerWithTitle: title message: message 638 | preferredStyle: UIAlertControllerStyleAlert]; 639 | [ac addTextFieldWithConfigurationHandler:^(UITextField* textField) { 640 | textField.placeholder = usernamePlaceholder; 641 | }]; 642 | [ac addTextFieldWithConfigurationHandler:^(UITextField* textField) { 643 | textField.placeholder = passwordPlaceholder; 644 | textField.secureTextEntry = YES; 645 | }]; 646 | aa = [UIAlertAction actionWithTitle: cancelButtonTitle style: UIAlertActionStyleCancel 647 | handler: ^(UIAlertAction*a) { 648 | receiver(nil, nil); 649 | }]; 650 | [ac addAction: aa]; 651 | aa = [UIAlertAction actionWithTitle: actionButtonTitle style: UIAlertActionStyleDefault 652 | handler: ^(UIAlertAction* action) { 653 | receiver(ac.textFields[0].text, ac.textFields[1].text); 654 | }]; 655 | [ac addAction: aa]; 656 | UIWindow* window = [UIApplication sharedApplication].keyWindow; 657 | UIViewController* vc = window.rootViewController; // walk down tree? 658 | [vc presentViewController: ac animated: YES completion: nil]; 659 | #else 660 | enum { 661 | tf_un=0, tf_pw, tf_count, 662 | frame_un=tf_un, frame_pw=tf_pw, frame_av, frame_count, 663 | }; 664 | NSTextField* username; 665 | NSTextField* password; 666 | NSTextField* tf; 667 | NSUInteger i; 668 | CGRect frame[frame_count] = {{2,28,280,20},{2,0,280,20},{0,0,284,48}}; 669 | for (i = 0; i < tf_count; i++) 670 | { 671 | if (i == tf_un) 672 | tf = username = [[NSTextField alloc] initWithFrame: frame[i]]; 673 | else 674 | tf = password = [[NSSecureTextField alloc] initWithFrame: frame[i]]; 675 | tf.placeholderString = i == tf_un ? usernamePlaceholder : passwordPlaceholder; 676 | tf.maximumNumberOfLines = 1; 677 | tf.drawsBackground = YES; tf.bordered = YES; tf.editable = YES; tf.selectable = YES; 678 | } 679 | NSView* accessoryView = [[NSView alloc] initWithFrame: frame[frame_av]]; 680 | [accessoryView addSubview: username]; 681 | [accessoryView addSubview: password]; 682 | 683 | NSAlert* alert = [[NSAlert alloc] init]; 684 | alert.messageText = title; 685 | alert.informativeText = message; 686 | [alert addButtonWithTitle: actionButtonTitle]; 687 | [alert addButtonWithTitle: cancelButtonTitle]; 688 | alert.accessoryView = accessoryView; 689 | [alert layout]; 690 | 691 | [alert beginSheetModalForWindow: NSApp.keyWindow 692 | completionHandler: 693 | ^(NSModalResponse returnCode) { 694 | if (returnCode == NSAlertFirstButtonReturn) 695 | { 696 | receiver(username.stringValue, password.stringValue); 697 | } 698 | } 699 | ]; 700 | #endif 701 | }; 702 | 703 | ReceiveDirectLoginParamsBlock receiver = 704 | ^void(NSString* username, NSString* password) 705 | { 706 | [self getAuthTokenWithParams: username : password]; 707 | }; 708 | 709 | provider(receiver); 710 | } 711 | - (void)getAuthTokenWithParams:(NSString*)username :(NSString*)password 712 | { 713 | if (![username length] || ![password length]) 714 | { 715 | [self endWithState: OBPSessionStateInvalid 716 | data: nil 717 | error: [NSError errorWithDomain: NSCocoaErrorDomain code: NSUserCancelledError userInfo: nil]]; 718 | return; 719 | } 720 | 721 | NSDictionary* d = _serverInfo.accessData; 722 | NSString* base = d[OBPServerInfo_AuthServerBase]; 723 | NSString* path = @"/my/logins/direct"; 724 | NSString* consumerKey = d[OBPServerInfo_ClientKey]; 725 | NSString* header; 726 | STHTTPRequest* request; 727 | STHTTPRequest __weak* request_ifStillAround; 728 | 729 | path = [base stringForURLByAppendingPath: path]; 730 | request_ifStillAround = request = [STHTTPRequest requestWithURLString: path]; 731 | request.HTTPMethod = @"POST"; 732 | request.POSTDictionary = @{}; 733 | 734 | header = [NSString stringWithFormat: @"DirectLogin username=\"%@\", password=\"%@\", consumer_key=\"%@\"", username, password, consumerKey]; 735 | [request setHeaderWithName: @"Authorization" value: header]; 736 | 737 | request.completionBlock = 738 | ^(NSDictionary *headers, NSString *body) 739 | { 740 | STHTTPRequest* request = request_ifStillAround; 741 | NSInteger status = request.responseStatus; 742 | NSDictionary* response; 743 | NSString* token; 744 | BOOL completedStage = NO; 745 | NSError* error = nil; 746 | 747 | if (status == 200) 748 | { 749 | response = [NSJSONSerialization JSONObjectWithData: [body dataUsingEncoding: NSUTF8StringEncoding] options: 0 error: &error]; 750 | token = response[@"token"]; 751 | if ([token length]) 752 | { 753 | [self endWithState: OBPSessionStateValid 754 | data: @{ OBPServerInfo_TokenKey : token, 755 | OBPServerInfo_TokenSecret : DL_TOKEN_SECRET} 756 | error: nil]; 757 | completedStage = YES; 758 | } 759 | } 760 | 761 | if (!completedStage) 762 | { 763 | OBP_LOG(@"getAuthToken request completion not successful: status=%d headers=%@ body=%@", (int)status, headers, body); 764 | [self endWithState: OBPSessionStateInvalid 765 | data: @{OBPServerInfo_TokenKey : @"", OBPServerInfo_TokenSecret : @""} 766 | error: [NSError errorWithDomain: OBPSessionErrorDomain code: OBPSessionErrorCompletionUnsuccessful userInfo: @{@"status":@(status), NSURLErrorKey:request?request.url:[NSNull null]}]]; 767 | } 768 | }; 769 | 770 | request.errorBlock = 771 | ^(NSError *error) 772 | { 773 | STHTTPRequest *request = request_ifStillAround; 774 | // Add server-side description to error if available. (STHTTPRequest enhancement) 775 | error = request ? [request errorByAddingServerSideDescriptionToError: error] : error; 776 | OBP_LOG(@"getAuthToken got error %@", error); 777 | [self endWithState: OBPSessionStateInvalid 778 | data: @{OBPServerInfo_TokenKey : @"", OBPServerInfo_TokenSecret : @""} 779 | error: [NSError errorWithDomain: OBPSessionErrorDomain code: OBPSessionErrorCompletionError userInfo: @{NSUnderlyingErrorKey:error,NSURLErrorKey:request_ifStillAround.url?:[NSNull null]}]]; 780 | }; 781 | 782 | _state = OBPSessionStateValidating; 783 | [request startAsynchronous]; 784 | self.valid = _state == OBPSessionStateValid; 785 | } 786 | - (void)addAuthorizationHeader2ToSTHTTPRequest:(STHTTPRequest*)request 787 | { 788 | NSDictionary* d = _serverInfo.accessData; 789 | NSString* tokenKey = d[OBPServerInfo_TokenKey]; 790 | NSString* tokenSecret = d[OBPServerInfo_TokenSecret]; 791 | NSString* header; 792 | 793 | OBP_ASSERT([tokenKey length] * [DL_TOKEN_SECRET isEqualToString: tokenSecret]); 794 | 795 | header = [NSString stringWithFormat: @"DirectLogin token=\"%@\"", tokenKey]; 796 | 797 | [request setHeaderWithName: @"Authorization" value: header]; 798 | } 799 | - (void)addAuthorizationHeader2ToURLRequest:(NSMutableURLRequest*)request 800 | { 801 | NSDictionary* d = _serverInfo.accessData; 802 | NSString* tokenKey = d[OBPServerInfo_TokenKey]; 803 | NSString* tokenSecret = d[OBPServerInfo_TokenSecret]; 804 | NSString* header; 805 | 806 | OBP_ASSERT([tokenKey length] * [DL_TOKEN_SECRET isEqualToString: tokenSecret]); 807 | 808 | header = [NSString stringWithFormat: @"DirectLogin token=\"%@\"", tokenKey]; 809 | 810 | [request setValue: header forHTTPHeaderField: @"Authorization"]; 811 | } 812 | @end 813 | 814 | 815 | -------------------------------------------------------------------------------- /OBPKit/Connection/OBPWebViewProvider.h: -------------------------------------------------------------------------------- 1 | // 2 | // OBPWebViewProvider.h 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 24/01/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | 16 | 17 | typedef BOOL(^OBPWebNavigationFilter)(NSURL*); // return YES if the target URL has been reached 18 | typedef void(^OBPWebCancelNotifier)(void); // 19 | 20 | 21 | 22 | /// Protocol OBPWebViewProvider declares the tasks that OBPSession needs performed in order to facilitate the user authentication stage of the OAuth authorisation sequence. 23 | @protocol OBPWebViewProvider 24 | 25 | - (NSString*)callbackScheme; ///< Recommend the URL scheme that should be used for OAuth callbacks (i.e. when redirecting with the result of user's login); this can be a simple scheme for embedded web views, but if the provider shows an external webview, then the scheme needs to be one which the OS recognises as exclusively handled by this app. 26 | 27 | - (void)showURL:(NSURL*)url filterNavWith:(OBPWebNavigationFilter)navigationFilter notifyCancelBy:(OBPWebCancelNotifier)canceled; ///< Show url in a webview, pass page navigation and redirects through navigationFilter, and call cancelled if the web view is closed by the user. \param url locates the web page in which the user will authorise client access to his/her resources. \param navigationFilter should be called with every new url to be loaded in the page, and will return YES when the authorisation callback url is detected, signifying that the provider should close the webview. \param cancel should be called if the user closes the webview. 28 | 29 | - (void)resetWebViewProvider; ///< Called to close the webview (if possible) when an incomplete auth request has been canceled or abandoned for some reason. 30 | 31 | @end 32 | 33 | 34 | 35 | /** 36 | Class OBPDefaultWebViewProvider implements minimum web view provider functionality, accessible through a singleton instance. 37 | 38 | With no configuration, it will bring up a basic in-app web view, but you can also configure it use an external browser. What are the differences? The in-app web browser is the most basic. An external browser is fully featured and gives security helpers that the user may rely on, i.e. offering to fetch appropriate account and password pairs from secure keychain, browser or third party storage — this is a very big deal for the eighty percent of users who are non-technical. However, iOS may also interrupt the callback handling from an external browser, by asking the user to confirm whether they want to return to the original app, which can be confusing to the user (who should not have to know anything about internal workings of OAuth). 39 | 40 | If you want to use an external browser, you must ensure that your application's info.plist file declares URL schemes that your app will recognise and respond to and that are unique to your app, e.g. by including... 41 | 42 | \code 43 | CFBundleURLTypes 44 | 45 | 46 | CFBundleURLName 47 | callback 48 | CFBundleTypeRole 49 | Viewer 50 | CFBundleURLSchemes 51 | 52 | x-${PRODUCT_BUNDLE_IDENTIFIER:lower} 53 | 54 | 55 | 56 | \endcode 57 | 58 | ...and then passing the name of the scheme ('callback' in the example above) to the configuration call +configureToUseExternalWebViewer:withCallbackSchemeName:andInstallHandlerHook:. If you do not already handle incoming URLs then you can ask for a handler hook to be installed, otherwise you must add a call to +handleCallbackURL: in your existing handler. 59 | */ 60 | @interface OBPDefaultWebViewProvider : NSObject 61 | 62 | + (instancetype)instance; ///< Access the singleton instance of this class. 63 | 64 | + (void)configureToUseExternalWebViewer:(BOOL)useExternal withCallbackSchemeName:(nullable NSString*)callbackSchemeName andInstallCallbackHook:(BOOL)installCallbackHook; ///< Configure how OBPDefaultWebViewProvider's singleton instance will behave. By default, it will use an in-app webview. \param useExternal will when YES, cause a URLs to be shown in the system's default web viewer, or when NO, cause an in-app web view to be brought up. \param callbackSchemeName identifies which entry in the main bundle's CFBundleURLTypes array to find by its CFBundleURLName. If found, then the first of the CFBundleURLSchemes in the entry is used. If not found, then bad things will happen as this parameter is mandatory when using an external web view view. It is optional for in-app web views. \param installCallbackHook will when YES, and useExternal is also YES, request that the singleton instance installs a hook function to receive the callback messages from the system and then forward them to +handleCallbackURL:. This is available on OSX, and will replace any existing message handler. However, it is currently not available on iOS, as it is prevented by security measures, so you must call +handleCallbackURL: directly instead, and the install request just performs a verification. If useExternal is YES and installCallbackHook is NO, then you are indicating that you will call +handleCallbackURL: directly. On OSX, do this from an apple event handler that you have installed, which handles events with class kInternetEventClass and id kAEGetURL (the URL is the direct object). On iOS, do this from either -[UIApplicationDelegate application:openURL:options:] or -[UIApplicationDelegate application:openURL:sourceApplication:annotation:]. 65 | 66 | + (BOOL)handleCallbackURL:(NSURL*)url; ///< Let OBPDefaultWebViewProvider check if this url is a callback it has been waiting for and handle it. \param url is a URL received on iOS through either -[UIApplicationDelegate application:openURL:options:] or -[UIApplicationDelegate application:openURL:sourceApplication:annotation:], or on OSX though your handler for apple events with class kInternetEventClass and id kAEGetURL. \returns YES if url was handled. 67 | 68 | + (NSString*)callbackSchemeFromBundleIdentifier; ///< Helper that constructs and returns a callback scheme of the form x-, converted to lower case and with illegal characters removed. You can use this if user auth is requested via an in-app web view. 69 | + (NSString*)callbackSchemeWithName:(NSString*)schemeName; ///< Helper that retrieves the named callback URL scheme from the bundle, if present. You need to use this (and therefore need to have added your schemes to the bundle) if user auth is requested via a web view external to the app. 70 | 71 | @end 72 | 73 | 74 | 75 | NS_ASSUME_NONNULL_END 76 | -------------------------------------------------------------------------------- /OBPKit/Connection/OBPWebViewProvider.m: -------------------------------------------------------------------------------- 1 | // 2 | // OBPWebViewProvider.m 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 24/01/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import "OBPWebViewProvider.h" 10 | // sdk 11 | #if TARGET_OS_IPHONE 12 | #import 13 | #include 14 | #else 15 | #import 16 | #endif 17 | #import 18 | // prj 19 | #import "OBPLogging.h" 20 | 21 | 22 | 23 | @interface OBPDefaultWebViewProvider () 24 | + (BOOL)installCallbackHook:(BOOL)install; 25 | @property (nonatomic, assign) BOOL useExternal; 26 | @property (nonatomic, strong) WKWebView* webView; 27 | - (void)showURL:(NSURL *)url; // make webview, set self as delegate, install in suitable host, show 28 | - (void)doneAndClose:(BOOL)close; // pull down host 29 | - (void)resetWebViewProvider; // clear members 30 | @property (nonatomic, strong) OBPWebNavigationFilter filterNav; 31 | @end 32 | 33 | 34 | 35 | #pragma mark - 36 | #if !TARGET_OS_IPHONE 37 | #define OBPWebViewProviderOS OBPWebViewProvider_OSX 38 | @interface OBPWebViewProvider_OSX : OBPDefaultWebViewProvider 39 | @end 40 | #endif 41 | 42 | #if TARGET_OS_IPHONE 43 | #define OBPWebViewProviderOS OBPWebViewProvider_iOS 44 | @class OBPWebViewProviderVC; 45 | 46 | @interface OBPWebViewProvider_iOS : OBPDefaultWebViewProvider 47 | - (void)webViewProviderVCDidClose:(OBPWebViewProviderVC*)vc; 48 | @end 49 | 50 | @interface OBPWebViewProviderVC : UIViewController 51 | @property (nonatomic, weak) OBPWebViewProvider_iOS* owner; 52 | @end 53 | #endif 54 | 55 | 56 | 57 | #pragma mark - 58 | @implementation OBPDefaultWebViewProvider 59 | { 60 | NSString* _callbackSchemeName; 61 | NSString* _callbackScheme; 62 | OBPWebNavigationFilter _filterNav; 63 | OBPWebCancelNotifier _notifyCancel; 64 | NSURL* _initialURL; 65 | } 66 | static OBPDefaultWebViewProvider* sOBPWebViewProvider = nil; 67 | + (void)initialize 68 | { 69 | if (self != [OBPDefaultWebViewProvider class]) 70 | return; 71 | sOBPWebViewProvider = [[OBPWebViewProviderOS alloc] initPrivate]; 72 | } 73 | + (instancetype)instance 74 | { 75 | return sOBPWebViewProvider; 76 | } 77 | + (void)configureToUseExternalWebViewer:(BOOL)useExternal 78 | withCallbackSchemeName:(NSString*)callbackSchemeName 79 | andInstallCallbackHook:(BOOL)installCallbackHook 80 | { 81 | NSString* scheme = [self callbackSchemeWithName: callbackSchemeName]; 82 | 83 | if (![scheme length]) 84 | { 85 | // Throwing an exception for errors here doesn't help when this has been called from -[NSApplicationDelegate applicationDidFinishLaunching:] because the exception is silently caught and the app is left in an indeterminate state. So we need brute force in DEBUG mode (in release builds, app will fall back to using internal web view)... 86 | if ([callbackSchemeName length]) 87 | { 88 | OBP_LOG(@"Bundle CFBundleURLTypes does not contain any CFBundleURLSchemes with CFBundleURLName of \"%@\". Ensure that there are entries and that the correct name is used. This is mandatory.", callbackSchemeName); 89 | #if DEBUG 90 | abort(); 91 | #endif 92 | } 93 | else 94 | if (useExternal) 95 | { 96 | OBP_LOG(@"%@", @"A callbackSchemeName parameter is mandatory when using an external web viewer, and must match the CFBundleURLName of one of the CFBundleURLSchemes in the CFBundleURLTypes entry in the main bundle info dictionary."); 97 | #if DEBUG 98 | abort(); 99 | #endif 100 | } 101 | useExternal = NO; 102 | scheme = sOBPWebViewProvider->_callbackScheme; 103 | } 104 | else 105 | if ([scheme rangeOfString: @"[^-+.A-Za-z0-9]+" options: NSRegularExpressionSearch].length) 106 | { 107 | OBP_LOG(@"Scheme %@ contains illegal characters. Should be [A-Za-z][-+.A-Za-z0-9]+ (RFC2396 Appendix A). Also advisable to use lowercase — some servers make this conversion leading to round-trip mismatch.", scheme); 108 | #if DEBUG 109 | abort(); 110 | #endif 111 | useExternal = NO; 112 | scheme = sOBPWebViewProvider->_callbackScheme; 113 | } 114 | else 115 | if (![scheme isEqualToString: [scheme lowercaseString]]) 116 | { 117 | OBP_LOG(@"Although uppercase characters are legal in schemes (RFC2396 Appendix A), callback schemes can be converted to lowercase by some servers, leading to a mismatch when filtering for a callback. Convert %@ to lowercase.", scheme); 118 | #if DEBUG 119 | abort(); 120 | #endif 121 | useExternal = NO; 122 | scheme = sOBPWebViewProvider->_callbackScheme; 123 | } 124 | 125 | installCallbackHook &= useExternal; 126 | 127 | if ([[sOBPWebViewProvider class] installCallbackHook: installCallbackHook]) 128 | { 129 | sOBPWebViewProvider->_callbackScheme = scheme; 130 | sOBPWebViewProvider->_useExternal = useExternal; 131 | } 132 | } 133 | + (NSString*)callbackSchemeWithName:(NSString*)schemeName 134 | { 135 | if (![schemeName length]) 136 | return nil; 137 | /* Info dictionary should contain a section like this in order to declare which URL schemes it can handle, and hence can be sent to it by the system. 138 | CFBundleURLTypes 139 | 140 | 141 | CFBundleURLName 142 | aName 143 | CFBundleTypeRole 144 | Viewer 145 | CFBundleURLSchemes 146 | 147 | aScheme 148 | 149 | 150 | 151 | */ 152 | NSArray *schemes, *urlTypes = [NSBundle mainBundle].infoDictionary[@"CFBundleURLTypes"]; 153 | NSString *name, *scheme; 154 | NSDictionary *d; 155 | for (d in urlTypes) 156 | { 157 | name = d[@"CFBundleURLName"]; 158 | if ([name isEqualToString: schemeName]) 159 | { 160 | schemes = d[@"CFBundleURLSchemes"]; 161 | scheme = [schemes firstObject]; 162 | return scheme; 163 | } 164 | } 165 | return nil; 166 | } 167 | + (NSString*)callbackSchemeFromBundleIdentifier 168 | { 169 | NSString* scheme; 170 | // RFC2396 sheme = [A-Za-z][A-Za-z0-9.-+]* 171 | scheme = [@"x-" stringByAppendingString: [NSBundle mainBundle].bundleIdentifier]; 172 | scheme = [scheme lowercaseString]; // uppercase is allowed but some servers return scheme converted to lowercase leading to roundtrip mismatch 173 | // Remove characters not allowed in schemes by RFC2396 174 | NSRange rg; 175 | while ((rg = [scheme rangeOfString: @"[^-+.A-Za-z0-9]+" options: NSRegularExpressionSearch]).length) 176 | scheme = [scheme stringByReplacingCharactersInRange: rg withString: @""]; 177 | return scheme; 178 | } 179 | + (BOOL)installCallbackHook:(BOOL)install 180 | { 181 | return NO; 182 | } 183 | + (BOOL)handleCallbackURL:(NSURL*)url 184 | { 185 | return [sOBPWebViewProvider handleCallbackURL: url]; 186 | } 187 | #pragma mark - 188 | - (instancetype)init {self = nil; return self;} // the designated non-initialiser 189 | - (instancetype)initPrivate 190 | { 191 | if (nil == (self = [super init])) 192 | return nil; 193 | _callbackScheme = [[self class] callbackSchemeFromBundleIdentifier]; 194 | return self; 195 | } 196 | - (NSString*)callbackScheme 197 | { 198 | return _callbackScheme; 199 | } 200 | - (void)showURL:(NSURL*)url 201 | filterNavWith:(OBPWebNavigationFilter)navigationFilter 202 | notifyCancelBy:(OBPWebCancelNotifier)cancelNotifier 203 | { 204 | if (nil == url || nil == navigationFilter || nil == cancelNotifier) 205 | return; 206 | _initialURL = url; 207 | _filterNav = navigationFilter; 208 | _notifyCancel = cancelNotifier; 209 | [self showURL: url]; 210 | } 211 | - (void)showURL:(NSURL*)url 212 | { 213 | // subclass imp 214 | } 215 | - (void)doneAndClose:(BOOL)close 216 | { 217 | if (_notifyCancel) 218 | _notifyCancel(); 219 | [self resetWebViewProvider]; 220 | } 221 | - (void)resetWebViewProvider 222 | { 223 | _notifyCancel = nil; 224 | _filterNav = nil; 225 | self.webView = nil; 226 | } 227 | - (BOOL)handleCallbackURL:(NSURL*)url // return route for external web viewing 228 | { 229 | if (_filterNav && _filterNav(url)) 230 | { 231 | _notifyCancel = nil; 232 | [self resetWebViewProvider]; 233 | } 234 | return NO; 235 | } 236 | - (void)setWebView:(WKWebView*)webView 237 | { 238 | if (_webView == webView) 239 | return; 240 | if (_webView) 241 | _webView.UIDelegate = nil, _webView.navigationDelegate = nil; 242 | _webView = webView; 243 | if (_webView) 244 | _webView.UIDelegate = self, _webView.navigationDelegate = self; 245 | } 246 | #pragma mark - WKUIDelegate 247 | - (void)webViewDidClose:(WKWebView *)webView 248 | { 249 | OBP_LOG_IF(0, @"[%@ webViewDidClose: %@]", self, webView); 250 | [self doneAndClose: YES]; 251 | } 252 | #pragma mark - WKNavigationDelegate 253 | - (void)webView:(WKWebView*)webView decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler 254 | { 255 | WKNavigationActionPolicy policy = WKNavigationActionPolicyAllow; 256 | WKNavigationType navType = navigationAction.navigationType; 257 | NSURL* navURL; 258 | 259 | OBP_LOG_IF(0, @"\nnavigationAction: %@", navigationAction); 260 | 261 | if (navType == WKNavigationTypeLinkActivated 262 | || navType == WKNavigationTypeOther) 263 | { 264 | navURL = navigationAction.request.URL; 265 | if (_filterNav(navURL)) 266 | { 267 | _notifyCancel = nil; 268 | policy = WKNavigationActionPolicyCancel; 269 | dispatch_async( 270 | dispatch_get_main_queue(), 271 | ^{[self doneAndClose: YES];} 272 | ); 273 | } 274 | else 275 | if (![_initialURL.host isEqualToString: navURL.host] 276 | || ![_initialURL.path isEqualToString: navURL.path]) 277 | { 278 | // Pass external links to system appointed browser 279 | policy = WKNavigationActionPolicyCancel; 280 | #if TARGET_OS_IPHONE 281 | [[UIApplication sharedApplication] openURL: navURL]; 282 | #else 283 | [[NSWorkspace sharedWorkspace] openURL: navURL]; 284 | #endif 285 | } 286 | } 287 | 288 | decisionHandler(policy); 289 | } 290 | @end 291 | 292 | 293 | 294 | #pragma mark - 295 | #if !TARGET_OS_IPHONE 296 | @implementation OBPWebViewProvider_OSX 297 | { 298 | NSWindow* _window; 299 | } 300 | - (void)makeWebView 301 | { 302 | enum {kAuthScreenStdWidth = 1068, kAuthScreenStdHeight = 724}; 303 | WKWebView* webView; 304 | NSScreen* mainScreen = [NSScreen mainScreen]; 305 | CGSize size = mainScreen.frame.size; 306 | CGRect contentRect = {.origin={0,0}, .size=size}; 307 | contentRect = CGRectInset(contentRect, MAX(0, (size.width - kAuthScreenStdWidth)/2), 308 | MAX(0, (size.height - kAuthScreenStdHeight)/2)); 309 | _window = [[NSWindow alloc] initWithContentRect: contentRect 310 | styleMask: NSTitledWindowMask 311 | + NSClosableWindowMask 312 | + NSResizableWindowMask 313 | + NSUnifiedTitleAndToolbarWindowMask 314 | + NSFullSizeContentViewWindowMask 315 | backing: NSBackingStoreRetained 316 | defer: YES 317 | screen: mainScreen]; 318 | _window.delegate = self; 319 | _window.releasedWhenClosed = NO; 320 | 321 | contentRect.origin = CGPointZero; 322 | self.webView = webView = [[WKWebView alloc] initWithFrame: contentRect]; 323 | webView.configuration.preferences.javaScriptCanOpenWindowsAutomatically = NO; 324 | 325 | _window.contentView = webView; 326 | [_window makeFirstResponder: webView]; 327 | } 328 | - (void)showURL:(NSURL*)url 329 | { 330 | if (self.useExternal) 331 | { 332 | [[NSWorkspace sharedWorkspace] openURL: url]; 333 | } 334 | else 335 | { 336 | if (!self.webView) 337 | [self makeWebView]; 338 | [_window makeKeyAndOrderFront: self]; 339 | [self.webView loadRequest: [NSURLRequest requestWithURL: url]]; 340 | } 341 | } 342 | - (void)doneAndClose:(BOOL)close 343 | { 344 | if (close) 345 | { 346 | _window.delegate = nil; 347 | [_window close]; 348 | } 349 | 350 | [super doneAndClose: close]; 351 | } 352 | - (void)resetWebViewProvider 353 | { 354 | [super resetWebViewProvider]; 355 | if (_window) 356 | _window.delegate = nil, _window = nil; 357 | } 358 | #pragma mark - NSWindowDelegate 359 | - (void)windowWillClose:(NSNotification*)notification 360 | { 361 | OBP_LOG_IF(0, @"[%@ windowWillClose: %@]", self, notification); 362 | [self doneAndClose: NO]; 363 | } 364 | #pragma mark - 365 | + (BOOL)installCallbackHook:(BOOL)install 366 | { 367 | static BOOL sInstalled = NO; 368 | if (sInstalled == install) 369 | return YES; 370 | 371 | NSAppleEventManager* aeMgr = [NSAppleEventManager sharedAppleEventManager]; 372 | if (install) 373 | [aeMgr setEventHandler: self andSelector: @selector(handleURLEvent:withReplyEvent:) 374 | forEventClass: kInternetEventClass andEventID: kAEGetURL]; 375 | else 376 | [aeMgr removeEventHandlerForEventClass: kInternetEventClass andEventID: kAEGetURL]; 377 | 378 | sInstalled = install; 379 | return YES; 380 | } 381 | + (void)handleURLEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent 382 | { 383 | if (event.eventClass != kInternetEventClass 384 | || event.eventID != kAEGetURL) 385 | return; 386 | NSAppleEventDescriptor* objDesc = [event paramDescriptorForKeyword: keyDirectObject]; 387 | NSString* urlStr = [objDesc stringValue]; 388 | if ([urlStr length]) 389 | [self handleCallbackURL: [NSURL URLWithString: urlStr]]; 390 | } 391 | @end 392 | #endif // !TARGET_OS_IPHONE 393 | 394 | 395 | 396 | #pragma mark - 397 | #if TARGET_OS_IPHONE 398 | @implementation OBPWebViewProviderVC 399 | { 400 | UIView* _rootView; 401 | UIToolbar* _toolbar; 402 | WKWebView* _webView; 403 | } 404 | - (instancetype)initWithOwner:(OBPWebViewProvider_iOS*)owner 405 | { 406 | if (nil == (self = [super initWithNibName: nil bundle: nil])) 407 | return nil; 408 | self.owner = owner; 409 | self.modalPresentationStyle = UIModalPresentationFullScreen; 410 | return self; 411 | } 412 | - (void)loadView 413 | { 414 | UIView* rootView; 415 | UIToolbar* toolbar; 416 | WKWebView* webView; 417 | CGRect availableArea = [UIScreen mainScreen].bounds; 418 | CGRect frame = {.origin = CGPointZero, .size = availableArea.size}; 419 | NSDictionary* views; 420 | NSDictionary* metrics; 421 | NSMutableArray* constraints; 422 | 423 | rootView = [[UIView alloc] initWithFrame: frame]; 424 | 425 | toolbar = [[UIToolbar alloc] init]; 426 | toolbar.items = @[ 427 | [[UIBarButtonItem alloc] initWithTitle: NSLocalizedString(@"Cancel", nil) style:UIBarButtonItemStylePlain target: self action: @selector(cancel:)], 428 | [[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemFlexibleSpace target: nil action: NULL] 429 | ]; 430 | frame = availableArea; 431 | frame.origin.y = frame.size.height - toolbar.intrinsicContentSize.height; 432 | frame.size.height = toolbar.intrinsicContentSize.height; 433 | toolbar.frame = frame; 434 | 435 | frame = availableArea; 436 | frame.size.height -= toolbar.intrinsicContentSize.height; 437 | webView = [[WKWebView alloc] initWithFrame: frame]; 438 | webView.configuration.preferences.javaScriptCanOpenWindowsAutomatically = NO; 439 | 440 | [rootView addSubview: webView]; 441 | [rootView addSubview: toolbar]; 442 | 443 | views = NSDictionaryOfVariableBindings(webView, toolbar); 444 | metrics = @{}; 445 | constraints = [NSMutableArray array]; 446 | #define AddConstraintsFor(fmt) [constraints addObjectsFromArray: [NSLayoutConstraint constraintsWithVisualFormat: fmt options: 0 metrics: metrics views: views]] 447 | AddConstraintsFor(@"H:|[webView]|"); 448 | AddConstraintsFor(@"H:|[toolbar]|"); 449 | AddConstraintsFor(@"V:|[webView][toolbar]|"); 450 | #undef AddConstraintsFor 451 | [NSLayoutConstraint activateConstraints: constraints]; 452 | [rootView addConstraints: constraints]; 453 | 454 | _toolbar = toolbar; 455 | _webView = webView; 456 | self.view = _rootView = rootView; 457 | self.owner.webView = _webView; 458 | } 459 | - (void)setView:(UIView*)view 460 | { 461 | if (view == nil) 462 | _webView = nil, _toolbar = nil, _rootView = nil; 463 | [super setView: view]; 464 | } 465 | - (void)cancel:(id)sender 466 | { 467 | [self dismissViewControllerAnimated: YES completion: ^{ 468 | [self->_owner webViewProviderVCDidClose: self]; 469 | }]; 470 | } 471 | @end 472 | 473 | 474 | 475 | #pragma mark - 476 | @implementation OBPWebViewProvider_iOS 477 | { 478 | OBPWebViewProviderVC* _vc; 479 | } 480 | - (void)webViewProviderVCDidClose:(OBPWebViewProviderVC*)vc 481 | { 482 | [self doneAndClose: NO]; 483 | } 484 | - (UIViewController*)topVC 485 | { 486 | UIViewController *vc, *topVC = nil; 487 | UIWindow *window, *topWindow = nil; 488 | for (window in [UIApplication sharedApplication].windows) 489 | { 490 | if (!window.isHidden) 491 | if (window.windowLevel == UIWindowLevelNormal) 492 | if (window.screen == [UIScreen mainScreen]) 493 | topWindow = window; 494 | } 495 | vc = topWindow.rootViewController; 496 | while (vc) 497 | { 498 | topVC = vc; 499 | if ([vc isKindOfClass: [UINavigationController class]]) 500 | vc = [(UINavigationController*)vc topViewController]; 501 | else 502 | vc = [vc.childViewControllers lastObject]; 503 | } 504 | return topVC; 505 | } 506 | - (void)showURL:(NSURL*)url 507 | { 508 | if (self.useExternal) 509 | [[UIApplication sharedApplication] openURL: url]; 510 | else 511 | { 512 | _vc = [[OBPWebViewProviderVC alloc] initWithOwner: self]; 513 | [[self topVC] presentViewController: _vc animated: YES completion: ^{ 514 | [self.webView loadRequest: [NSURLRequest requestWithURL: url]]; 515 | }]; 516 | } 517 | } 518 | - (void)doneAndClose:(BOOL)close 519 | { 520 | if (close) 521 | [_vc dismissViewControllerAnimated: YES completion: ^{ 522 | [super doneAndClose: close]; 523 | }]; 524 | else 525 | [super doneAndClose: close]; 526 | } 527 | - (void)resetWebViewProvider 528 | { 529 | [super resetWebViewProvider]; 530 | if (_vc) 531 | { 532 | if (_vc.presentingViewController) 533 | [_vc dismissViewControllerAnimated: NO completion: ^{}]; 534 | _vc.owner = nil, _vc = nil; 535 | } 536 | } 537 | #pragma mark - 538 | + (BOOL)installCallbackHook:(BOOL)install 539 | { 540 | static BOOL sInstalled = NO; 541 | if (sInstalled == install) 542 | return YES; 543 | id appDelegate = [UIApplication sharedApplication].delegate; 544 | SEL sel; 545 | Method method; 546 | BOOL methodAdded; 547 | 548 | if (install) 549 | { 550 | if ((0)) 551 | { 552 | // Approach A. 553 | // Installing an implementaion for -application:openURL:options: or -application:openURL:sourceApplication:annotation: does not work. The installation is successful (test calls pass through ok), but the UIApplication object does not send these messages to the delegate, even though the app is correctly reactivated by a callback from an external browser. The implication is that only the state at the time of instantiation of the app delegate is honoured (if it isn't an oversight, then it could be a security feature to thwart code injection after launch). Hence, we are patching too late. Unwaranted complexity to do it earlier. 554 | 555 | for (int n = 0; n < 2; n++) 556 | { 557 | sel = n ? @selector(application:openURL:options:) 558 | : @selector(application:openURL:sourceApplication:annotation:); 559 | // Cant tail patch, so complain if there is already a handler 560 | if ([appDelegate respondsToSelector: sel]) 561 | { 562 | #if DEBUG 563 | OBP_LOG(@"%@ already implements %@. Call +[OBPDefaultWebViewProvider handleCallbackURL:] from within the implementation, because +[%@ %@] will not patch it to make the call.", NSStringFromClass([appDelegate class]), NSStringFromSelector(sel), NSStringFromClass([self class]), NSStringFromSelector(_cmd)); 564 | abort(); 565 | #endif 566 | return NO; 567 | } 568 | } 569 | NSInteger OSv = [[[UIDevice currentDevice].systemVersion componentsSeparatedByString: @"."][0] integerValue]; 570 | sel = OSv >= 9 ? @selector(application:openURL:options:) 571 | : @selector(application:openURL:sourceApplication:annotation:); 572 | methodAdded = NO; 573 | Class class_AppDelegate = [appDelegate class]; 574 | if (class_AppDelegate) 575 | if (NULL == (method = class_getInstanceMethod(class_AppDelegate, sel))) 576 | if (NULL != (method = class_getInstanceMethod(self, sel))) 577 | if (class_addMethod(class_AppDelegate, 578 | sel, method_getImplementation(method), method_getTypeEncoding(method))) 579 | { 580 | method = class_getInstanceMethod(class_AppDelegate, sel); 581 | methodAdded = method != nil; 582 | } 583 | if (!methodAdded) 584 | return NO; 585 | } 586 | else 587 | { 588 | // Approach B 589 | // Force the developer to implement -application:openURL:options: or -application:openURL:sourceApplication:annotation: and to correctly call +[OBPDefaultWebViewProvider handleCallbackURL:] from within it. 590 | NSURL* loopTestURL = [NSURL URLWithString: [[sOBPWebViewProvider callbackScheme] stringByAppendingString: @"://OBPLoopTest"]]; 591 | __block BOOL passed = NO; 592 | sOBPWebViewProvider.filterNav = ^BOOL(NSURL* url) { 593 | BOOL match = [url isEqual: loopTestURL]; 594 | passed |= match; 595 | return match; 596 | }; 597 | if ([appDelegate respondsToSelector: @selector(application:openURL:options:)]) 598 | { 599 | [appDelegate application: [UIApplication sharedApplication] openURL: loopTestURL options: @{}]; 600 | OBP_LOG_IF(!passed, @"%@", @"To use OBPDefaultWebViewProvider with an external web viewer, you must test +[OBPDefaultWebViewProvider handleCallbackURL:] from within your UIApplicationDelegate implementation of -application:openURL:options:"); 601 | } 602 | else 603 | if ([appDelegate respondsToSelector: @selector(application:openURL:sourceApplication:annotation:)]) 604 | { 605 | [appDelegate application: [UIApplication sharedApplication] openURL: loopTestURL sourceApplication: nil annotation: @{}]; 606 | OBP_LOG_IF(!passed, @"%@", @"To use OBPDefaultWebViewProvider with an external web viewer, you must test +[OBPDefaultWebViewProvider handleCallbackURL:] from within your UIApplicationDelegate implementation of -application:openURL:sourceApplication:annotation:"); 607 | } 608 | else 609 | OBP_LOG(@"%@", @"To use OBPDefaultWebViewProvider with an external web viewer, your UIApplicationDelegate must implement -application:openURL:sourceApplication:annotation: or -application:openURL:options: and test +[OBPDefaultWebViewProvider handleCallbackURL:] early within the implementation."); 610 | sOBPWebViewProvider.filterNav = nil; 611 | if (!passed) 612 | { 613 | #if DEBUG 614 | abort(); 615 | #endif 616 | return NO; 617 | } 618 | } 619 | } 620 | else 621 | { 622 | // really? 623 | return NO; 624 | } 625 | sInstalled = install; 626 | return YES; 627 | } 628 | // Note: these two method implementation will be swizzled into place, after which self will be a different class, so do not reference self either implicitly or explicitly 629 | - (BOOL)application:(UIApplication*)a openURL:(NSURL*)u sourceApplication:(NSString*)s annotation:(id)n 630 | { 631 | return [OBPDefaultWebViewProvider handleCallbackURL: u]; 632 | } 633 | - (BOOL)application:(UIApplication*)a openURL:(NSURL*)u options:(NSDictionary*)o 634 | { 635 | return [OBPDefaultWebViewProvider handleCallbackURL: u]; 636 | } 637 | @end 638 | #endif // !TARGET_OS_IPHONE 639 | 640 | 641 | 642 | -------------------------------------------------------------------------------- /OBPKit/Marshal/OBPMarshal.h: -------------------------------------------------------------------------------- 1 | // 2 | // OBPMarshal.h 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 18/02/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | 16 | 17 | @class OBPSession; 18 | 19 | 20 | 21 | static NSString* const OBPMarshalErrorDomain = @"OBPMarshalErrorDomain"; 22 | NS_ENUM(NSInteger) { OBPMarshalErrorUnexpectedResourceKind = 4192, 23 | OBPMarshalErrorUnexpectedResult = 4193, 24 | }; 25 | 26 | static NSString* const OBPMarshalOptionOnlyPublicResources = @"onlyPublic"; ///< OBPMarshalOptionOnlyPublicResources key for options dictionary, value of type NSNumber interpreted as BOOL, where value YES indicates omit authorization in order to act on only the publicly available resources and value NO (default when omitted) indicates add authorisation so as to act on both the privately available resources of the authorised user and the publicly available resources. 27 | static NSString* const OBPMarshalOptionSendDictAsForm = @"serializeToJSON"; ///< OBPMarshalOptionSendDictAsForm key for options dictionary, value of type NSNumber interpreted as BOOL, where value YES indicates send a dictionary payload as "application/x-www-form-urlencoded", and value NO (same as omitting the option) indicates serialize the payload (for POST/PUT...) as a JSON object; a payload of type NSData is always sent as raw data. 28 | static NSString* const OBPMarshalOptionExtraHeaders = @"extraHeaders"; ///< OBPMarshalOptionExtraHeaders key for options dictionary, value of type dictionary, containing extra header key-value pairs that are acceptable for the particular API call, e.g. for sorting, and subranging by date and/or ordinal; header values supplied as NSDate and NSNumber will be converted to correct string values. 29 | static NSString* const OBPMarshalOptionExpectClass = @"expectClass"; ///< OBPMarshalOptionExpectClass key for options dictionary, value of type Class is the expected class of the deserialized JSON object, pass [NSNull null] or [NSNull class] to signify no fixed expectation; if omited, then desrialized object is expected to be an NSDictionary; mismatch is treated as OBPMarshalErrorUnexpectedResourceKind 30 | static NSString* const OBPMarshalOptionExpectStatus = @"expectStatus"; ///< OBPMarshalOptionExpectStatus key for options dictionary, value of type NSNumber or array of NSNumber giving the expected normal response status code(s); when omitted, the default expectations are 201 for POST, 204 for DELETE, 200 for others. 31 | static NSString* const OBPMarshalOptionDeserializeJSON = @"deserializeJSON"; ///< OBPMarshalOptionDeserializeJSON key for options dictionary, value of type NSNumber interpreted as BOOL and indicating whether to deserialize the response body as a JSON object: value YES is the same as omitting the option; use NO to suppress. 32 | static NSString* const OBPMarshalOptionErrorHandler = @"errorHandler"; ///< OBPMarshalOptionErrorHandler key for options dictionary, value of type HandleOBPMarshalError block gives alternative error handler to the standard handler. 33 | 34 | 35 | 36 | typedef void(^HandleOBPMarshalError)(NSError* error, NSString* path); // (error, path) 37 | typedef void(^HandleOBPMarshalData)(id deserializedObject, NSString* responseBody); // (deserializedObject, responseBody) 38 | 39 | 40 | 41 | /** Class OBPMarshal helps you marshal resources through the OBP API with get (GET), create (POST), update (PUT) and delete (DELETE) operations. Paths are always relative to the OBP API base. There must always be a supplied error handler or a default error handler. You can obtain a default instance from an OBPSession instance, or create your own. 42 | 43 | An OBPMarshal instance will: 44 | 45 | - use private API calls, i.e. with authorisation by the session object. You can make public API call (no authorisation) by adding OBPMarshalOptionOnlyPublicResources : @YES to your options dictionary. 46 | 47 | - use its own error handler, unless you supply one as a parameter, or by adding OBPMarshalOptionErrorHandler : yourErrorHandler to the options dictionary; the parameter is chosen in preference to the option. 48 | 49 | - send a resource (create, update) supplied as NSData as the raw data, and a resource supplied as any other valid JSON root object kind as its serialised JSON description. To send a dictionary resource as a form, include OBPMarshalOptionSendDictAsForm : @YES in your options dictionary. 50 | 51 | - accept a response with status code 201 for create (POST), 204 for DELETE, and 200 for get and update (GET, PUT), and will reject the response if the status is different. To specify one or more status codes to accept instead of the defaults, add OBPMarshalOptionExpectStatus key with a single NSNumber or an NSArray of NSNumber to your options dictionary, e.g. OBPMarshalOptionExpectStatus : @212, or OBPMarshalOptionExpectStatus : @[@201, @212]. 52 | 53 | - if the response body is non-empty, expect it to be a serialized object in JSON format, will deserialize it for you and will reject the response if deserialised object is not a dictionary. To prevent deserialisation, add OBPMarshalOptionDeserializeJSON : @NO to your options dictionary. To expect a different class of JSON root object include OBPMarshalOptionExpectClass : class in your options dictionary. To suppress class checking, add OBPMarshalOptionExpectClass : [NSNull null]. 54 | 55 | To add extra headers that modify the action of the call, add OBPMarshalOptionExtraHeaders : headerDictionary to your options dictionary. For example, to page transactions with get /banks/BANK_ID/accounts/ACCOUNT_ID/VIEW_ID/transactions, you can add OBPMarshalOptionExtraHeaders : @{@"obp_limit":@(chunkSize), @"obp_offset":@(nextChunkOffset)}. Note that OBPMarshal will convert any NSDate values you pass to strings using OBPDateFormatter. 56 | */ 57 | @interface OBPMarshal : NSObject 58 | @property (nonatomic, strong) HandleOBPMarshalError errorHandler; ///< Get/set a default error handler block for this instance. 59 | @property (nonatomic, weak, readonly) OBPSession* session; ///< Get the session object that this instance exclusively works with, identifying the OBP server with which it communicates. 60 | 61 | - (instancetype)initWithSessionAuth:(OBPSession*)session; ///< Designated initialiser. session parameter is mandatory. Sets a default error handler which simply logs the error in Debug builds. 62 | 63 | - (BOOL)getResourceAtAPIPath:(NSString*)path withOptions:(nullable NSDictionary*)options forResultHandler:(HandleOBPMarshalData)resultHandler orErrorHandler:(nullable HandleOBPMarshalError)errorHandler; ///< Request the resource at path from API base (GET), passing the result to handler, or errors to the error handler. \param path identifies the resource to get, relative to the API base URL. \param options may supply key-value pairs to customise behaviour. \returns YES if the request was launched, or NO if the session or parameters were invalid. \sa See the class description for details of default behaviour and how to override using the options parameter. 64 | 65 | - (BOOL)updateResource:(id)resource atAPIPath:(NSString*)path withOptions:(nullable NSDictionary*)options forResultHandler:(HandleOBPMarshalData)resultHandler orErrorHandler:(nullable HandleOBPMarshalError)errorHandler; ///< Update the resource at path from API base (PUT), passing the result to handler, or errors to the error handler. \param path identifies the resource to update, relative to the API base URL. \param options may supply key-value pairs to customise behaviour. \returns YES if the request was launched, or NO if the session or parameters were invalid. \sa See the class description for details of default behaviour and how to override using the options parameter. 66 | 67 | - (BOOL)createResource:(id)resource atAPIPath:(NSString*)path withOptions:(nullable NSDictionary*)options forResultHandler:(HandleOBPMarshalData)resultHandler orErrorHandler:(nullable HandleOBPMarshalError)errorHandler; ///< Create a resource at path from API base (POST), passing the result to handler, or errors to the error handler. \param path identifies the resource to create, relative to the API base URL. \param options may supply key-value pairs to customise behaviour. \returns YES if the request was launched, or NO if the session or parameters were invalid. \sa See the class description for details of default behaviour and how to override using the options parameter. 68 | 69 | - (BOOL)deleteResourceAtAPIPath:(NSString*)path withOptions:(nullable NSDictionary*)options forResultHandler:(HandleOBPMarshalData)resultHandler orErrorHandler:(nullable HandleOBPMarshalError)errorHandler; ///< Delete the resource at path from API base (DELETE), passing the result to handler, or errors to the error handler. \param options may supply key-value pairs to customise behaviour. \returns YES if the request was launched, or NO if the session or parameters were invalid. \sa See the class description for details of default behaviour and how to override using the options parameter. 70 | 71 | @end 72 | 73 | 74 | 75 | NS_ASSUME_NONNULL_END 76 | -------------------------------------------------------------------------------- /OBPKit/Marshal/OBPMarshal.m: -------------------------------------------------------------------------------- 1 | // 2 | // OBPMarshal.m 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 18/02/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import "OBPMarshal.h" 10 | // sdk 11 | // ext 12 | #import 13 | // prj 14 | #import "OBPServerInfo.h" 15 | #import "OBPSession.h" 16 | #import "NSString+OBPKit.h" 17 | #import "OBPLogging.h" 18 | #import "OBPDateFormatter.h" 19 | 20 | 21 | 22 | typedef NS_ENUM(uint8_t, OBPMarshalVerb) 23 | { 24 | eOBPMarshalVerb_GET, 25 | eOBPMarshalVerb_PUT, 26 | eOBPMarshalVerb_POST, 27 | eOBPMarshalVerb_DELETE, 28 | 29 | eOBPMarshalVerb_count 30 | }; 31 | 32 | 33 | 34 | NSString* NSStringDescribingNSURLRequest(NSURLRequest* request); 35 | NSString* NSStringDescribingNSURLResponseAndData(NSHTTPURLResponse* response, NSData* data); 36 | 37 | 38 | 39 | @implementation OBPMarshal 40 | - (instancetype)initWithSessionAuth:(OBPSession*)session 41 | { 42 | if (!session) 43 | {self = nil; return nil;} 44 | if (nil == (self = [super init])) 45 | return nil; 46 | _session = session; 47 | [self setDefaultErrorHandler]; 48 | return self; 49 | } 50 | - (void)setDefaultErrorHandler 51 | { 52 | __weak __typeof(self) self_ifStillAlive = self; 53 | _errorHandler = 54 | ^(NSError* error, NSString* path) 55 | { 56 | OBP_LOG(@"Request for resource at path %@ served by %@ got error %@", path, self_ifStillAlive.session.serverInfo.APIBase, error); 57 | }; 58 | } 59 | - (BOOL)getResourceAtAPIPath:(NSString*)p withOptions:(NSDictionary*)o forResultHandler:(HandleOBPMarshalData)rh orErrorHandler:(HandleOBPMarshalError)eh 60 | { 61 | if (eh == nil && o) 62 | eh = o[@"errorHandler"]; 63 | return [self sendRequestVerb: eOBPMarshalVerb_GET withPayload: nil toAPIPath: p withOptions: o forResultHandler: rh orErrorHandler: eh]; 64 | } 65 | - (BOOL)updateResource:(id)r atAPIPath:(NSString*)p withOptions:(NSDictionary*)o forResultHandler:(HandleOBPMarshalData)rh orErrorHandler:(HandleOBPMarshalError)eh 66 | { 67 | if (eh == nil && o) 68 | eh = o[@"errorHandler"]; 69 | return [self sendRequestVerb: eOBPMarshalVerb_PUT withPayload: r toAPIPath: p withOptions: o forResultHandler: rh orErrorHandler: eh]; 70 | } 71 | - (BOOL)createResource:(id)r atAPIPath:(NSString*)p withOptions:(NSDictionary*)o forResultHandler:(HandleOBPMarshalData)rh orErrorHandler:(HandleOBPMarshalError)eh 72 | { 73 | if (eh == nil && o) 74 | eh = o[@"errorHandler"]; 75 | return [self sendRequestVerb: eOBPMarshalVerb_POST withPayload: r toAPIPath: p withOptions: o forResultHandler: rh orErrorHandler: eh]; 76 | } 77 | - (BOOL)deleteResourceAtAPIPath:(NSString*)p withOptions:(NSDictionary*)o forResultHandler:(HandleOBPMarshalData)rh orErrorHandler:(HandleOBPMarshalError)eh 78 | { 79 | if (eh == nil && o) 80 | eh = o[@"errorHandler"]; 81 | return [self sendRequestVerb: eOBPMarshalVerb_DELETE withPayload: nil toAPIPath: p withOptions: o forResultHandler: rh orErrorHandler: eh]; 82 | } 83 | - (BOOL)sendRequestVerb:(OBPMarshalVerb)verb 84 | withPayload:(id)payload 85 | toAPIPath:(NSString*)path 86 | withOptions:(NSDictionary*)options 87 | forResultHandler:(HandleOBPMarshalData)resultHandler 88 | orErrorHandler:(HandleOBPMarshalError)errorHandler 89 | { 90 | OBPSession* session = _session; 91 | HandleOBPMarshalError eh = errorHandler ?: _errorHandler; 92 | 93 | if ((!session.valid && ![options[OBPMarshalOptionOnlyPublicResources] isEqual: @YES]) 94 | || ![path length] || !resultHandler || !eh) 95 | return NO; 96 | 97 | NSString* requestPath; 98 | STHTTPRequest* request; 99 | STHTTPRequest __weak* request_ifStillAround; 100 | NSString* method; 101 | Class expectedDeserializedObjectClass = [NSDictionary class]; 102 | id obj; 103 | BOOL onlyPublicResources = NO; 104 | BOOL sendDictAsForm = NO; 105 | BOOL serializeToJSON = YES; 106 | BOOL deserializeJSON = YES; 107 | BOOL verbose = [[NSUserDefaults standardUserDefaults] boolForKey: @"OBPMarshalVerbose"]; 108 | NSArray* acceptableStatusCodes; 109 | NSDictionary* dict; 110 | NSString* key; 111 | NSData* data; 112 | NSError* error; 113 | NSMutableDictionary* moreHeaders = [NSMutableDictionary dictionary]; 114 | 115 | // Method 116 | switch (verb) 117 | { 118 | case eOBPMarshalVerb_GET: 119 | method = @"GET"; 120 | acceptableStatusCodes = @[@200]; 121 | break; 122 | case eOBPMarshalVerb_PUT: 123 | method = @"PUT"; 124 | acceptableStatusCodes = @[@200]; 125 | break; 126 | case eOBPMarshalVerb_POST: 127 | method = @"POST"; 128 | acceptableStatusCodes = @[@201]; 129 | break; 130 | case eOBPMarshalVerb_DELETE: 131 | method = @"DELETE"; 132 | acceptableStatusCodes = @[@204]; 133 | break; 134 | default: 135 | return NO; 136 | } 137 | 138 | // Options 139 | if (options) 140 | { 141 | // Expected class of object after deserialisation 142 | obj = options[OBPMarshalOptionExpectClass]; 143 | if (obj) 144 | if ([obj isEqual: [NSNull null]] || obj == [NSNull class]) 145 | expectedDeserializedObjectClass = nil; 146 | else 147 | expectedDeserializedObjectClass = obj; 148 | 149 | // Make public calls? (suppress authorisation) 150 | obj = options[OBPMarshalOptionOnlyPublicResources]; 151 | if ([obj respondsToSelector: @selector(boolValue)]) 152 | onlyPublicResources = [obj boolValue]; 153 | 154 | // Send payload as form? 155 | obj = options[OBPMarshalOptionSendDictAsForm]; 156 | if ([obj respondsToSelector: @selector(boolValue)]) 157 | serializeToJSON = !(sendDictAsForm = [obj boolValue]); 158 | 159 | // Expect reply body is JSON and deserialize? 160 | obj = options[OBPMarshalOptionDeserializeJSON]; 161 | if ([obj respondsToSelector: @selector(boolValue)]) 162 | deserializeJSON = [obj boolValue]; 163 | 164 | // Expect non-default reply status code(s)? 165 | obj = options[OBPMarshalOptionExpectStatus]; 166 | if ([obj respondsToSelector: @selector(integerValue)]) 167 | acceptableStatusCodes = @[obj]; 168 | else 169 | if ([obj isKindOfClass: [NSArray class]]) 170 | acceptableStatusCodes = obj; 171 | 172 | // Add extra headers? 173 | dict = obj = options[OBPMarshalOptionExtraHeaders]; 174 | if ([obj isKindOfClass: [NSDictionary class]]) 175 | for (key in dict) 176 | { 177 | obj = dict[key]; 178 | if ([obj isKindOfClass: [NSDate class]]) 179 | moreHeaders[key] = [OBPDateFormatter stringFromDate: obj]; 180 | else 181 | moreHeaders[key] = [obj description]; 182 | } 183 | } 184 | 185 | // Make the request and add its payload 186 | requestPath = [session.serverInfo.APIBase stringForURLByAppendingPath: path]; 187 | request = [STHTTPRequest requestWithURLString: requestPath]; 188 | OBP_LOG_IF(!request, @"Unable to create request with path %@", requestPath); 189 | if (!request) 190 | return NO; 191 | request.HTTPMethod = method; 192 | 193 | if (payload) 194 | { 195 | if ([payload isKindOfClass: [NSData class]]) 196 | request.rawPOSTData = data = payload; 197 | else 198 | if (sendDictAsForm) 199 | { 200 | if ([payload isKindOfClass: [NSDictionary class]]) 201 | request.POSTDictionary = payload; 202 | else 203 | OBP_LOG(@"••• Payload needs to be a dictionary to send as a form; ignored: %@", payload); 204 | } 205 | else 206 | if (serializeToJSON) 207 | { 208 | data = [NSJSONSerialization dataWithJSONObject: payload options: 0 error: &error]; 209 | OBP_LOG_IF(error || !data, @"Payload JSON serialize failed with error %@\n for data %@", error, payload); 210 | if (data) 211 | { 212 | request.rawPOSTData = data; 213 | moreHeaders[@"Content-Type"] = @"application/json"; 214 | } 215 | } 216 | else 217 | OBP_LOG(@"••• Payload ignored: %@", payload); 218 | } 219 | 220 | if ([moreHeaders count]) 221 | [request.requestHeaders addEntriesFromDictionary: moreHeaders]; 222 | 223 | // Reply handler 224 | request_ifStillAround = request; 225 | request.completionBlock = ^(NSDictionary *headers, NSString *body) { 226 | STHTTPRequest *request = request_ifStillAround; 227 | NSInteger status = request.responseStatus; 228 | OBP_LOG_IF(verbose, @"\nResponse: %d\nHeaders: %@\nBody: %@", (int)status, headers, body); 229 | NSError* error = nil; 230 | BOOL handled = NO; 231 | if (NSNotFound != [acceptableStatusCodes indexOfObject: @(status)]) 232 | { 233 | id container = nil; 234 | if (deserializeJSON) 235 | { 236 | container = [NSJSONSerialization JSONObjectWithData: [body dataUsingEncoding: NSUTF8StringEncoding] options: 0 error: &error]; 237 | OBP_LOG_IF(error, @"[NSJSONSerialization JSONObjectWithData: data options: 0 error:] gave error:\nerror = %@\ndata = %@", error, body); 238 | OBP_LOG_IF(!error && expectedDeserializedObjectClass && ![container isKindOfClass: expectedDeserializedObjectClass], @"Expected to resource at path %@ to yield a %@, but got instead got:\n%@\nfrom body: %@", path, NSStringFromClass(expectedDeserializedObjectClass), container, body); 239 | if (!error && expectedDeserializedObjectClass && ![container isKindOfClass: expectedDeserializedObjectClass]) 240 | error = [NSError errorWithDomain: OBPMarshalErrorDomain code: OBPMarshalErrorUnexpectedResourceKind userInfo:@{NSLocalizedDescriptionKey:@"Unexpected response data type.",@"body":body?:@""}]; 241 | } 242 | if (!error) 243 | resultHandler(container, body), handled = YES; 244 | } 245 | else 246 | { 247 | OBP_LOG(@"Unexpected response (%@), when expecting %@; body = %@", @(status), acceptableStatusCodes, body); 248 | error = [NSError errorWithDomain: OBPMarshalErrorDomain code: OBPMarshalErrorUnexpectedResult userInfo:@{NSLocalizedDescriptionKey:[NSString stringWithFormat: @"Unexpected response status (%@).", @(status)],@"body":body?:@""}]; 249 | } 250 | if (!handled) 251 | eh(error, path); 252 | }; 253 | 254 | // Error handler 255 | request.errorBlock = ^(NSError *error) { 256 | eh(error, path); 257 | }; 258 | 259 | // Authorise and send 260 | if (onlyPublicResources || [session authorizeSTHTTPRequest: request]) 261 | { 262 | [request startAsynchronous]; 263 | OBP_LOG_IF(verbose, @"\n%@", NSStringDescribingNSURLRequest((NSURLRequest*)[(id)request performSelector: @selector(request)])); 264 | return YES; 265 | } 266 | 267 | return NO; 268 | } 269 | @end 270 | 271 | 272 | 273 | NSString* NSStringDescribingNSURLRequest(NSURLRequest* request) 274 | { 275 | // Snippet from: http://stackoverflow.com/a/31734423/618653 276 | NSMutableString *message = [NSMutableString stringWithString:@"---Request------------------\n"]; 277 | [message appendFormat:@"URL: %@\n",[request.URL description] ]; 278 | [message appendFormat:@"Method: %@\n",[request HTTPMethod]]; 279 | for (NSString *header in [request allHTTPHeaderFields]) 280 | { 281 | [message appendFormat:@"%@: %@\n",header,[request valueForHTTPHeaderField:header]]; 282 | } 283 | [message appendFormat:@"Body: %@\n",[[NSString alloc] initWithData:[request HTTPBody] encoding:NSUTF8StringEncoding]]; 284 | [message appendString:@"----------------------------\n"]; 285 | return [NSString stringWithFormat:@"%@",message]; 286 | }; 287 | 288 | NSString* NSStringDescribingNSURLResponseAndData(NSHTTPURLResponse* response, NSData* data) 289 | { 290 | // Snippet from: http://stackoverflow.com/a/31734423/618653 291 | NSString *responsestr = [[NSString alloc] initWithBytes:[data bytes] length:[data length] encoding:NSUTF8StringEncoding]; 292 | NSMutableString *message = [NSMutableString stringWithString:@"---Response------------------\n"]; 293 | [message appendFormat:@"URL: %@\n",[response.URL description] ]; 294 | [message appendFormat:@"MIMEType: %@\n",response.MIMEType]; 295 | [message appendFormat:@"Status Code: %ld\n",(long)response.statusCode]; 296 | for (NSString *header in [[response allHeaderFields] allKeys]) 297 | { 298 | [message appendFormat:@"%@: %@\n",header,[response allHeaderFields][header]]; 299 | } 300 | [message appendFormat:@"Response Data: %@\n",responsestr]; 301 | [message appendString:@"----------------------------\n"]; 302 | return [NSString stringWithFormat:@"%@",message]; 303 | }; 304 | 305 | 306 | -------------------------------------------------------------------------------- /OBPKit/Util/NSString+OBPKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+OBPKit.h 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 25/01/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | 13 | @interface NSString (OBPKit) 14 | - (NSString*)stringByAddingPercentEncodingForAllRFC3986ReservedCharachters; 15 | - (NSString*)stringByAppendingURLQueryParams:(NSDictionary*)dictionary; 16 | - (NSDictionary*)extractURLQueryParams; 17 | - (NSString*)stringForURLByAppendingPath:(NSString*)path; 18 | @end 19 | -------------------------------------------------------------------------------- /OBPKit/Util/NSString+OBPKit.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+OBPKit.m 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 25/01/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import "NSString+OBPKit.h" 10 | // sdk 11 | // prj 12 | #import "OBPLogging.h" 13 | 14 | 15 | 16 | @implementation NSString (OBPKit) 17 | - (NSString*)stringByAddingPercentEncodingForAllRFC3986ReservedCharachters 18 | { 19 | static NSCharacterSet* sAllowedSet = nil; 20 | static dispatch_once_t onceToken; 21 | dispatch_once(&onceToken, ^{ 22 | sAllowedSet = [NSCharacterSet characterSetWithCharactersInString: 23 | @"-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"]; 24 | // ...the Unreserved Characters in RFC 3986, Section 2.3. (Unfortunately the NSURLUtilities category on NSCharacterSet provides allowed sets for Host, User, Password, Path, Query and Fragment, but not the Unreserved set.) 25 | }); 26 | return [self stringByAddingPercentEncodingWithAllowedCharacters: sAllowedSet]; 27 | } 28 | - (NSString*)stringByAppendingURLQueryParams:(NSDictionary*)dictionary 29 | { 30 | NSMutableString* str = [self mutableCopy]; 31 | const char* sep = [str rangeOfString:@"?"].length ? "&" : "?"; 32 | 33 | for (id key in dictionary) 34 | { 35 | NSString *keyString = [key description]; 36 | NSString *valString = [dictionary[key] description]; 37 | keyString = [keyString stringByAddingPercentEncodingForAllRFC3986ReservedCharachters]; 38 | valString = [valString stringByAddingPercentEncodingForAllRFC3986ReservedCharachters]; 39 | [str appendFormat: @"%s%@=%@", sep, keyString, valString]; 40 | sep = "&"; 41 | } 42 | 43 | return [str copy]; 44 | } 45 | 46 | -(NSDictionary *)extractURLQueryParams 47 | { 48 | NSMutableDictionary *params = [NSMutableDictionary dictionary]; 49 | NSArray *pairs, *elements; 50 | NSString *pair, *key, *val; 51 | 52 | pairs = [self componentsSeparatedByString: @"&"]; 53 | 54 | for (pair in pairs) 55 | { 56 | elements = [pair componentsSeparatedByString: @"="]; 57 | OBP_LOG_IF(2 != [elements count], @"-extractQueryParams\nNot an element pair: %@\nQuery string: %@", pair, self); 58 | if ([elements count] != 2) 59 | continue; 60 | key = elements[0]; 61 | val = elements[1]; 62 | key = [key stringByRemovingPercentEncoding]; 63 | val = [val stringByRemovingPercentEncoding]; 64 | 65 | params[key] = val; 66 | } 67 | 68 | return [params copy]; 69 | } 70 | 71 | - (NSString*)stringForURLByAppendingPath:(NSString*)path 72 | { 73 | if (path == nil) 74 | return self; 75 | BOOL trailing = 0 != [self rangeOfString: @"/" options: NSAnchoredSearch+NSBackwardsSearch].length; 76 | BOOL leading = 0 != [path rangeOfString: @"/" options: NSAnchoredSearch].length; 77 | if (trailing && leading) // too many 78 | path = [path substringFromIndex: 1], leading = NO; 79 | else 80 | if (!trailing && !leading) // too few 81 | path = [@"/" stringByAppendingString: path], leading = YES; 82 | path = [self stringByAppendingString: path]; 83 | return path; 84 | } 85 | @end 86 | -------------------------------------------------------------------------------- /OBPKit/Util/OBPDateFormatter.h: -------------------------------------------------------------------------------- 1 | // 2 | // OBPDateFormatter.h 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 26/05/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | 13 | /// OBPDateFormatter deals only with the date formats used by the OBP API, which are a subset of the ISO 8601 format possibilities. 14 | @interface OBPDateFormatter : NSDateFormatter 15 | + (NSString*)stringFromDate:(NSDate*)date; 16 | + (NSDate*)dateFromString:(NSString*)string; 17 | @end 18 | -------------------------------------------------------------------------------- /OBPKit/Util/OBPDateFormatter.m: -------------------------------------------------------------------------------- 1 | // 2 | // OBPDateFormatter.m 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 26/05/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import "OBPDateFormatter.h" 10 | #import "OBPLogging.h" 11 | 12 | 13 | 14 | @implementation OBPDateFormatter 15 | static OBPDateFormatter* sInstA = nil; 16 | static OBPDateFormatter* sInstB = nil; 17 | + (void)initialize 18 | { 19 | if (self != [OBPDateFormatter class]) 20 | return; 21 | sInstA = [[self alloc] initWithSubseconds: NO]; 22 | sInstB = [[self alloc] initWithSubseconds: YES]; 23 | } 24 | + (NSString*)stringFromDate:(NSDate*)date 25 | { 26 | return [sInstA stringFromDate: date]; 27 | } 28 | + (NSDate*)dateFromString:(NSString*)string 29 | { 30 | NSDate* date = [sInstA dateFromString: string] ?: [sInstB dateFromString: string]; 31 | OBP_LOG_IF([string length] && !date, @"[%@ dateFromString: %@] • string format not recognised •", self, string); 32 | return date; 33 | } 34 | - (instancetype)initWithSubseconds:(BOOL)subsecs 35 | { 36 | if (nil != (self = [super init])) 37 | { 38 | self.timeZone = [NSTimeZone timeZoneForSecondsFromGMT: 0]; 39 | self.dateFormat = subsecs ? @"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" : @"yyyy-MM-dd'T'HH:mm:ss'Z'"; 40 | } 41 | return self; 42 | } 43 | @end 44 | -------------------------------------------------------------------------------- /OBPKit/Util/OBPLogging.h: -------------------------------------------------------------------------------- 1 | // 2 | // OBPLogging.h 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 24/01/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #ifndef OBPLogging_h 10 | #define OBPLogging_h 11 | // ...OBPLogging_h is inclusion guard 12 | 13 | #ifdef __OBJC__ 14 | #import 15 | #else 16 | #include 17 | #endif 18 | 19 | #ifndef OBP_LOG /* if not superceded by replacement definitions... */ 20 | #ifndef OBP_NO_LOGGING /* if logging not suppressed... */ 21 | 22 | /* OBP_LOG_BASE = bottleneck for all log output */ 23 | #define OBP_LOG_BASE(fmt, ...) do{NSLog(fmt, __VA_ARGS__);}while(0) 24 | 25 | /* OBP_LOG = normal route for output; always in debug; conditional and by default off in release */ 26 | #if DEBUG 27 | #define OBP_LOG(fmt, ...) OBP_LOG_BASE(fmt, ##__VA_ARGS__) 28 | #else 29 | #ifndef OBP_RELEASE_LOGGING 30 | #define OBP_RELEASE_LOGGING 0 31 | #endif 32 | #define OBP_LOG(fmt, ...) do{if(OBP_RELEASE_LOGGING)OBP_LOG_BASE(fmt, ##__VA_ARGS__);}while(0) 33 | #endif 34 | 35 | /* OBP_LOG_DR = exceptional route for output to be made in both in debug and release */ 36 | #define OBP_LOG_DR(fmt, ...) OBP_LOG_BASE(fmt, ##__VA_ARGS__) 37 | 38 | #else 39 | #define OBP_LOG_BASE(fmt, ...) 40 | #define OBP_LOG_DR(fmt, ...) 41 | #define OBP_LOG(fmt, ...) 42 | #endif 43 | #endif 44 | 45 | #ifndef OBP_BREAK 46 | #if DEBUG 47 | #if TARGET_CPU_X86 48 | #define OBP_BREAK do{if(1){__asm__("int $3\n" : : );}}while(0) 49 | #elif TARGET_CPU_X86_64 50 | #define OBP_BREAK do{if(1){__asm__("int $3\n" : : );}}while(0) 51 | #else 52 | #define OBP_BREAK do{}while(0) 53 | #endif 54 | #else 55 | #define OBP_BREAK do{}while(0) 56 | #endif 57 | #endif 58 | 59 | #define OBP_LOG_IF(test, fmt, ...) do{if(test)OBP_LOG(fmt, ##__VA_ARGS__);}while(0) 60 | #define OBP_ASSERT(test) do{if(!(test)){OBP_LOG(@"Assert %s failed (%s:%d)", #test, __PRETTY_FUNCTION__, __LINE__);OBP_BREAK;}}while(0) 61 | 62 | #endif /* OBPLogging_h */ 63 | -------------------------------------------------------------------------------- /OBPKit/Util/STHTTPRequest+Error.h: -------------------------------------------------------------------------------- 1 | // 2 | // STHTTPRequest+Error.h 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 16/06/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | 12 | 13 | @interface STHTTPRequest (Error) 14 | - (NSError*)errorByAddingServerSideDescriptionToError:(NSError*)inError; ///< Given an error just generated by an STHTTPRequest instance, check if the request already received a server-side description of the error, and if so create a new error with the server-side error description added. 15 | @end 16 | -------------------------------------------------------------------------------- /OBPKit/Util/STHTTPRequest+Error.m: -------------------------------------------------------------------------------- 1 | // 2 | // STHTTPRequest+Error.m 3 | // OBPKit 4 | // 5 | // Created by Torsten Louland on 16/06/2016. 6 | // Copyright (c) 2016-2017 TESOBE Ltd. All rights reserved. 7 | // 8 | 9 | #import "STHTTPRequest+Error.h" 10 | 11 | 12 | 13 | @implementation STHTTPRequest (Error) 14 | - (NSError*)errorByAddingServerSideDescriptionToError:(NSError*)inError 15 | { 16 | NSInteger status = inError.code; 17 | 18 | if (status < 400 19 | || status > 600 20 | || status != self.responseStatus 21 | || ![inError.domain isEqualToString: NSStringFromClass([self class])]) 22 | return inError; 23 | 24 | NSData* data = self.responseData; 25 | 26 | if (!data || ![data length]) 27 | return inError; 28 | 29 | NSDictionary* headers = self.responseHeaders; 30 | NSError* error = nil; 31 | NSString* errorDescription = inError.localizedDescription; 32 | NSString* serverSideDescription = nil; 33 | NSDictionary* userInfo = inError.userInfo; 34 | NSString* s; 35 | NSRange r; 36 | 37 | // If possible, expose the server's own description of the error... 38 | if ([(s = headers[@"Content-Type"]) hasPrefix: @"application/json"]) 39 | { 40 | id container = [NSJSONSerialization JSONObjectWithData: data options: 0 error: &error]; 41 | id serverSideInfo; 42 | if (container && [container isKindOfClass:[NSDictionary class]]) 43 | if (nil != (serverSideInfo = ((NSDictionary *)container)[@"error"])) 44 | serverSideDescription = [serverSideInfo description]; 45 | } 46 | else 47 | if ([(s = headers[@"Content-Type"]) hasPrefix: @"text/html"]) 48 | { 49 | // See if the body contains a simple error message that we can extract. Anything more complicated falls back to including the whole body below. 50 | s = [[NSString alloc] initWithData:data encoding: NSUTF8StringEncoding]; 51 | r = [s rangeOfString: @"<(body|BODY)>[^<]+" options: NSRegularExpressionSearch]; 52 | if (r.length) 53 | r.length -= 13, r.location += 6; 54 | if (r.length) 55 | serverSideDescription = [s substringWithRange: r]; 56 | } 57 | 58 | // When there was possibly server-side error information and it is in a form we don't cater for, optionally include header and data for debugging 59 | NSMutableDictionary* md = [(userInfo?:@{}) mutableCopy]; 60 | if (serverSideDescription) 61 | { 62 | // Don't add a second time (if STHTTPRequest is now handling this (pending)) 63 | if (0 == [errorDescription rangeOfString: serverSideDescription].length) 64 | errorDescription = [errorDescription stringByAppendingFormat:@" (%@)", serverSideDescription]; 65 | } 66 | else 67 | { 68 | md[@"headers"] = headers ?: @{}; 69 | md[@"data"] = data ? [[NSString alloc] initWithData:data encoding: NSUTF8StringEncoding] : @""; 70 | } 71 | if (errorDescription) 72 | md[NSLocalizedDescriptionKey] = errorDescription; 73 | userInfo = [md copy]; 74 | 75 | error = [NSError errorWithDomain: inError.domain code: inError.code userInfo: userInfo]; 76 | 77 | return error; 78 | } 79 | @end 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## OBPKit for OSX and iOS ![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg) 2 | 3 | TESOBE Ltd. 4 | 5 | 6 | 1. [Overview](#overview) 7 | 1. [Installation](#installation) 8 | 1. [Installation with Carthage](#installation-with-carthage) 9 | 1. [Installation with CocoaPods](#installation-with-cocoapods) 10 | 1. [Classes](#classes) 11 | 1. [OBPServerInfo](#obpserverinfo) 12 | 1. [OBPSession](#obpsession) 13 | 1. [OBPWebViewProvider](#obpwebviewprovider) 14 | 1. [OBPMarshal](#obpmarshal) 15 | 1. [OBPDateFormatter](#obpdateformatter) 16 | 1. [How to Use](#how-to-use) 17 | 1. [Callback Schemes](#callback-schemes) 18 | 1. [Customising OBPMarshal Behaviour](#customising-obpmarshal-behaviour) 19 | 20 | 21 | 22 | ### Overview 23 | 24 | OBPKit allows you to easily connect your existing iOS and OSX apps to servers offering the [Open Bank Project API][API]. 25 | 26 | It takes care of the authorisation process, and once the user has given your app permission to access his/her resources, it provides you with a helper to marshal resources through the API, or if you want to roll your own, you can use OBPKit to add authorisation headers to the requests you form yourself. 27 | 28 | You can look at the [HelloOBP-iOS][] and [HelloOBP-Mac][] sample applications to see OBPKit in use. 29 | 30 | 31 | 32 | ### Installation 33 | 34 | You can use either [Carthage][] or [CocoaPods][] ([more](https://cocoapods.org/about)). (If you are sitting on the fence, there is a way to support both until you are ready to decide based on experience. Link coming soon; ask for details if interested.) 35 | 36 | #### Installation with Carthage 37 | 38 | 1. [Install the latest Carthage][Carthage-install] — in a nutshell: install [homebrew](http://brew.sh), run `brew update` and run `brew install carthage`. 39 | 40 | 1. If you don't already have one, create a file named `Cartfile` at the root level of your project folder. To your Cartfile add... 41 | 42 | github "OpenBankProject/OBPKit-iOSX" 43 | 44 | 1. Run `carthage update --platform iOS,Mac` (omitting the platforms you do not need). This will add to your project's root directory, the directories Carthage/Checkouts, containing all the needed projects, and Carthage/Build/{iOS,Mac}, containing the frameworks built from the projects. It will also add a Cartfile.resolved file at root level. 45 | 46 | 1. In Xcode, open the project for your app, select your app target, go to the build phases tab and expand the `Embed Frameworks` phase, and drag in the OBPKit, OAuthCore, STHTTPRequest and UICKeyChainStore frameworks from the Carthage/Build/{iOS,Mac} subfolder for your app platform. 47 | 48 | 1. You will also need to add the Security framework to your `Link Binary With Libraries` build phase. 49 | 50 | 1. Add `Carthage/` to your `.gitignore` file before you commit this change. 51 | 52 | #### Installation with CocoaPods 53 | 54 | 1. [Install the latest CocoaPods][CocoaPods-install] 55 | 56 | 1. If you already have a Podfile, add… 57 | 58 | ```ruby 59 | pod 'OAuthCore', :git => 'https://github.com/t0rst/OAuthCore.git' 60 | # ...OBPKit currently requires the t0rst fork of OAuthCore 61 | pod 'OBPKit', :git => 'https://github.com/OpenBankProject/OBPKit-iOSX.git' 62 | ``` 63 | 64 | …to your target dependencies. Otherwise, create a file named `Podfile` at the root level of your project folder, with contents like… 65 | 66 | ```ruby 67 | platform :ios, '8.0' # delete if inappropriate 68 | platform :osx, '10.9' # delete if inappropriate 69 | target 'your-app-target-name' do 70 | pod 'OAuthCore', :git => 'https://github.com/t0rst/OAuthCore.git' 71 | # ...OBPKit currently requires the t0rst fork of OAuthCore 72 | pod 'OBPKit', :git => 'https://github.com/OpenBankProject/OBPKit-iOSX.git' 73 | end 74 | ``` 75 | 76 | 1. Run `pod install`. This will modify your yourproj.xcproj, add yourproj.xcworkspace and Podfile.lock, and add a Pods folder at the root level of your project folder. Use yourproj.xcworkspace from now on. 77 | 78 | 1. Add `Pods/` to your `.gitignore` file before you commit this change. 79 | 80 | 81 | 82 | ### Classes 83 | 84 | There are three main classes to use, one protocol to adopt and some helpers. 85 | 86 | #### OBPServerInfo 87 | 88 | An `OBPServerInfo` *instance* records the data necessary to access an OBP server. It stores sensitive credentials securely in the key chain. 89 | 90 | The `OBPServerInfo` *class* keeps a persistent record of all complete instances. An instance is complete once its client key and secret have been set. You can typically obtain these for your app from https://host-serving-OBP-API/consumer-registration. 91 | 92 | You can use the `OBPServerInfo` class to keep a record of all the OBP servers for which you support a connection; usually you will just have one, but more are possible. `OBPServerInfo` instances are reloaded automatically when your app is launched. 93 | 94 | A default instance of the helper class `OBPServerInfoStorage` handles the actual save and restore. You can customise your storage approach by nominating that your override class be used. You can configure this, as well as other security details, by passing a dictionary of customisation options to the function `OBPServerInfoCustomise` before the `OBPServerInfo` class initialize has been called. 95 | 96 | #### OBPSession 97 | 98 | You request an `OBPSession` instance for the OBP server you want to connect to, and use it to handle the authorisation sequence, and once access is gained, use the session's marshall object to help you marshal resources through the API. 99 | 100 | By default, `OBPSession` uses OAuth for authorisation, which is the correct way for your app to gain your user's permission to access his/her resources, and OBPKit makes this very easy to use. However, during initial development, you can also set the `authMethod` of your instance to use [Direct Login][DirectLogin] if you wish. (You can also set the `authMethod` to none in order to restrict access to just the public resources on an OBP server; or do this as needed for individual calls to OBPMarshall.) 101 | 102 | The `OBPSession` class keeps track of the instances that are currently alive, and will create or retrieve an `OBPSession` instance for an `OBPServerInfo` instance identifying a server you want to talk to. Both `OBPServerInfo` and `OBPSession` allow you to access default instances for when you only want to deal with singletons. 103 | 104 | #### OBPWebViewProvider 105 | 106 | For OAuth, `OBPSession` needs some part of your app to act as an `OBPWebViewProvider` protocol adopter in order to show the user a web page when it is time to get authorisation to access his/her resources. 107 | 108 | If you don't provide an `OBPWebViewProvider` protocol adopter, then the `OBPDefaultWebViewProvider` class singleton will be used. It provides basic support, and you can choose whether an in-app or external web view is brought up by calling configuring with the class member `+configureToUseExternalWebViewer:withCallbackSchemeName:andInstallCallbackHook:`. There are advantages and disadvantages to both, as set out in the Xcode quick help for the class. 109 | 110 | See [Callback Schemes](#callback-schemes) below for important additional considerations. 111 | 112 | #### OBPMarshal 113 | 114 | A default `OBPMarshal` instance is available from your `OBPSession` object, and will take care of creating, fetching, updating and deleting resources from the API, with response data and corresponding deserialised object delivered to your completion blocks. You can easily override the default behaviour by invoking a range of options. You can also supply extra headers via the options to, for example, limit by ordinal or date the range of transactions you retrieve. 115 | 116 | | REST API Verb | OBPMarshal Call | 117 | | --- | --- | 118 | | GET | `-getResourceAtAPIPath:withOptions:forResultHandler:orErrorHandler:` | 119 | | POST | `-createResource:atAPIPath:withOptions:forResultHandler:orErrorHandler:` | 120 | | PUT | `-updateResource:atAPIPath:withOptions:forResultHandler:orErrorHandler:` | 121 | | DELETE | `-deleteResourceAtAPIPath:withOptions:forResultHandler:orErrorHandler:` | 122 | 123 | #### OBPDateFormatter 124 | 125 | You can use the `OBPDateFormatter` helper to convert back and forth between `NSDate` instances and the string representation used when sending and recieving OBP API resources. 126 | 127 | 128 | 129 | ### How to Use 130 | 131 | The [HelloOBP-iOS][] and [HelloOBP-Mac][] sample apps demonstrate simple use of the OBPKit classes. 132 | 133 | In your app delegate after start-up, check for and create the `OBPServerInfo` instance for the main server you will connect to (if it hasn't already been restored from a previous run): 134 | 135 | ```objc 136 | if (nil == [OBPServerInfo firstEntryForAPIServer: kDefaultServer_APIBase]) 137 | { 138 | OBPServerInfo* serverInfo; 139 | serverInfo = [OBPServerInfo addEntryForAPIServer: kDefaultServer_APIBase]; 140 | serverInfo.data = DefaultServerDetails(); 141 | } 142 | ``` 143 | 144 | Here the details of the default server are fetched from a simple header (DefaultServerDetails.h), which is insecure, but in production, you might want to give the API keys some stronger protection. While getting up and running, this kind of thing is sufficient: 145 | 146 | ```objc 147 | static NSString* const kDefaultServer_APIBase = @"https://apisandbox.openbankproject.com/obp/v2.0.0/"; 148 | NS_INLINE NSDictionary* DefaultServerDetails() { 149 | return @{ 150 | OBPServerInfo_APIBase : kDefaultServer_APIBase, 151 | OBPServerInfo_AuthServerBase : @"https://apisandbox.openbankproject.com/", 152 | OBPServerInfo_ClientKey : @"0iz1zuscashkoyd3ztzb3i5whuubzetfihfc52ve", 153 | OBPServerInfo_ClientSecret : @"p4li5mklyt1h42w5u0tx4w2evtn2yz3gmntyn2ty", 154 | }; 155 | } 156 | ``` 157 | 158 | In your main or starting view, create the OBPSession instances that you want to work with: 159 | 160 | ```objc 161 | if (_session == nil) 162 | { 163 | OBPServerInfo* serverInfo = [OBPServerInfo firstEntryForAPIServer: chosenServer_APIBase]; 164 | _session = [OBPSession sessionWithServerInfo: serverInfo]; 165 | } 166 | ``` 167 | 168 | When the user requests to log in, ask the default session instance to validate, i.e. get authorisation for accessing resources on behalf of the client: 169 | 170 | ```objc 171 | - (void)viewDidLoad 172 | { 173 | ... 174 | 175 | // Kick off session authentication 176 | [[OBPSession currentSession] validate: 177 | ^(NSError* error) 178 | { 179 | if (error == nil) // success, do stuff... 180 | [self fetchAccounts]; 181 | } 182 | ]; 183 | } 184 | ``` 185 | 186 | After this, you can use the marshal property of the current session instance to retrieve resources from the API: 187 | 188 | ```objc 189 | OBPMarshal* marshal = [OBPSession currentSession].marshal; 190 | NSString* path = [NSString stringWithFormat: @"banks/%@/accounts/private", bankID]; 191 | HandleOBPMarshalData responseHandler = 192 | ^(id deserializedObject, NSString* responseBody) 193 | { 194 | _accountsDict = deserializedObject; 195 | [self loadAccounts]; 196 | }; 197 | 198 | [marshal getResourceAtAPIPath: path 199 | withOptions: nil 200 | forResponseHandler: responseHandler 201 | orErrorHandler: nil]; // nil ==> use default error handler 202 | ``` 203 | 204 | #### Callback Schemes 205 | 206 | As part of the OAuth handshake, the API auth server will redirect to a callback URL that your app specifies. OBPKit handles this for you by using a default callback at `x-${PRODUCT_BUNDLE_IDENTIFIER:lower}://callback`. For example. the [HelloOBP-iOS][] bundle identifier is `com.tesobe.HelloOBP-iOS` and it therefore use a default callback of `x-com.tesobe.helloobp-ios://callback`. You need only take further action if you require a different callback. However, there are two important points to note: 207 | 208 | 1. The bundle identifier of your app should use characters that are legal in [RFC3986-Schemes][], i.e. `[-+.A-Za-z0-9]`; if this is not possible or desirable, then use a custom callback scheme. 209 | 1. For extra security, the API auth server requires that the callback URL your app specifies for redirect is the same as the one you gave when you registered your app as an API Consumer and got API keys in return. You can change the registered redirect URL at a later time (keep a note of the consumer id for this), but to save you the trouble, consider the bundle id you will use before registering and construct your callback URL accordingly. 210 | 211 | When using an external web view (as mentioned above in [OBPWebViewProvider](#obpwebviewprovider)), iOS and macOS use the scheme to uniquely identify the app that should receive the redirect URL. iOS and macOS detect these schemes by looking in your app bundle's info dictionary, hence you should add a section to your `info.plist` file as follows (viewing as source): 212 | 213 | ```plist 214 | CFBundleURLTypes 215 | 216 | 217 | CFBundleURLName 218 | callback 219 | CFBundleTypeRole 220 | Viewer 221 | CFBundleURLSchemes 222 | 223 | x-${PRODUCT_BUNDLE_IDENTIFIER:lower} 224 | 225 | 226 | 227 | ``` 228 | 229 | You can replace the bundle-derived scheme with your own custom scheme if need be, and likewise use a different callback name if necessary. `OBPDefaultWebViewProvider` helper function `+callbackSchemeWithName:` retrieves the scheme from the bundle info dictionary for you. 230 | 231 | ### Customising OBPMarshal Behaviour 232 | 233 | Most of the time, the default behaviour of `OBPMarshal` is what you will want, but for special situations you can easily override the default `OBPMarshal` behaviour by passing an options dictionary in with your calls: 234 | 235 | | To… | add the key… | with value… | 236 | | :--- | :--- | :--- | 237 | | …access only public resources | `OBPMarshalOptionOnlyPublicResources` | `@YES` | 238 | | …add extra headers | `OBPMarshalOptionExtraHeaders` | `@{ @"obp_limit" : @(chunkSize), @"obp_offset" : @(nextChunkOffset) }` (…for example) | 239 | | …expect a response body that isn't JSON | `OBPMarshalOptionDeserializeJSON` | `@NO` | 240 | | …expect a JSON container object that is not a dictionary | `OBPMarshalOptionExpectClass` | `[NSArray class]` (…for example) | 241 | | …expect a JSON container object that could be any class | `OBPMarshalOptionExpectClass` | `[NSNull null]` | 242 | | …expect a non-default HTTP status code | `OBPMarshalOptionExpectStatus` | `@201` | 243 | | …accept several HTTP status codes | `OBPMarshalOptionExpectStatus` | `@[@201, @212]` | 244 | | …send a form instead of JSON | `OBPMarshalOptionSendDictAsForm` | `@YES` | 245 | 246 | 247 | 248 | [OBP]: http://www.openbankproject.com 249 | [API]: https://github.com/OpenBankProject/OBP-API/wiki 250 | [DirectLogin]: https://github.com/OpenBankProject/OBP-API/wiki/Direct-Login 251 | [HelloOBP-iOS]: https://github.com/OpenBankProject/Hello-OBP-OAuth1.0a-IOS 252 | [HelloOBP-Mac]: https://github.com/OpenBankProject/Hello-OBP-OAuth1.0a-Mac 253 | [Carthage]: https://github.com/Carthage/Carthage/blob/master/README.md 254 | [Carthage-install]: https://github.com/Carthage/Carthage/blob/master/README.md#installing-carthage 255 | [CocoaPods]: https://github.com/CocoaPods/CocoaPods/blob/master/README.md 256 | [CocoaPods-install]: http://guides.cocoapods.org/using/getting-started.html#installation 257 | [RFC3986-Schemes]: https://tools.ietf.org/html/rfc3986#section-3.1 258 | --------------------------------------------------------------------------------