├── .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)>[^<]+(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 
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 |
--------------------------------------------------------------------------------