Created
May 16, 2017 12:28
-
-
Save robypag/fcce9711c0fcfaa0e1c7ce9545dbc44f to your computer and use it in GitHub Desktop.
Implementation of a user session manager that handles the communication back and forth between iOS App and the SAP Cloud Platform Mobile Services
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SCPSessionManager.swift | |
// | |
// Created by Roberto Pagni on 15/05/17. | |
// Copyright © 2017 Coderp. All rights reserved. | |
// | |
// Wrapper for the basic functionalities of SCPms. | |
// The class will handle the communication back and forth between the Application and the | |
// Cloud Platform mobile services environment, managing tasks like registration of a new device, | |
// checking validity of application registrations and authorization challenges. | |
// It acts also as a proxy for the OData service provider. | |
// NOTE: OData classes MUST BE GENERATED using the SAP HCP SDK for iOS Assistant | |
// Version 1 is handling ONLY Basic Authorization based requests. SAML and OAuth are planned | |
import UIKit | |
import SAPCommon | |
import SAPFoundation | |
import SAPOData | |
protocol SCPSessionManagerDelegate : class{ | |
// The Application ID that the session manager should address | |
var applicationId: String {get set} | |
// The CPMS Instance URL that the session manager should use | |
var cpmsUrlString: String {get set} | |
// The application connection id that the session manager should use | |
var applicationConnectionId: String? {get set} | |
// Called when the session manager creates the URL session for the first time | |
// Can be used to add Observers, additional headers or to replace the inner URLRequest | |
func session(_: SCPSessionManager, applicationConnectionIdFor session: SAPURLSession) -> String | |
// Called on negative response after connection check: | |
func session(_: SCPSessionManager, checkDidFailWithResponse response: URLResponse, withError error: Error?) | |
// Called on positive response after connection check: | |
func session(_: SCPSessionManager, checkDidSuccessWithResponse response: URLResponse) | |
// Called on negative response after registration attempt: | |
func session(_: SCPSessionManager, registerDidFailWithResponse response: URLResponse, withData data: Data, withError error: Error?) | |
// Called on positive response after registration attempt: | |
func session(_: SCPSessionManager, registerDidSuccessWithId applicationConnectionId: String) | |
} | |
// Possible errors raised by the Cloud Platform | |
enum SCPSessionManagerError: Error{ | |
case EmptyCredentials | |
case InvalidCredentials | |
case ServiceUnavailable | |
case ApplicationConnectionIdRefused | |
case ApplicationConnectionIdNotInPayload | |
case EndpointNotFound | |
case UnknownNetworkError | |
} | |
// Session Manager class main implementation | |
class SCPSessionManager: SAPURLSessionDelegate { | |
// Delegate Object | |
weak var delegate: SCPSessionManagerDelegate? | |
// Logger Object | |
private let logger: Logger = Logger.shared(named: "SCPSessionLogger") | |
// SAP URL Session Object | |
private var sapUrlSession: SAPURLSession? | |
// SAP CPms Endpoints | |
private var sapcpmsUrl: URL? | |
private var appUrl: URL? | |
private var registrationUrl: URL? | |
private var configurationParameters: SAPcpmsSettingsParameters? | |
// Current device informations: | |
private static let registrationInfo = [ | |
"DeviceType": UIDevice.current.model, | |
"DeviceModel": "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)", | |
"UserName": UIDevice.current.name | |
] | |
// Username and password | |
private var userName: String = "" | |
private var password: String = "" | |
// Initialization: | |
init(withDelegate delegate: SCPSessionManagerDelegate ){ | |
// Store the delegate property | |
self.delegate = delegate | |
// Prepare Logger | |
do { | |
Logger.root.logLevel = LogLevel.error | |
try SAPcpmsLogUploader.attachToRootLogger() | |
} catch { | |
self.logger.error("Unable to attach to Root Logger") | |
} | |
// Observe the session | |
self.sapUrlSession = SAPURLSession(delegate: self) | |
self.sapUrlSession?.register(SAPcpmsObserver(settingsParameters: Constants.configurationParameters)) | |
// Build endpoints | |
self.sapcpmsUrl = URL(string: (self.delegate?.cpmsUrlString)!) | |
self.appUrl = self.sapcpmsUrl?.appendingPathComponent((self.delegate?.applicationId)!) | |
self.registrationUrl = self.sapcpmsUrl?.appendingPathComponent("odata/applications/latest/\((self.delegate?.applicationId)!)/Connections") | |
self.configurationParameters = SAPcpmsSettingsParameters(backendURL: self.sapcpmsUrl!, applicationID: (self.delegate?.applicationId)!) | |
} | |
// On destructor, upload logs | |
deinit { | |
let configurationParameters = SAPcpmsSettingsParameters(backendURL: Constants.sapcpmsUrl, applicationID: Constants.appId) | |
SAPcpmsLogUploader.uploadLogs(sapURLSession: self.sapUrlSession!, settingsParameters: configurationParameters) | |
} | |
// On start session | |
public func startSession(forUser user: String, withPassword password: String ) throws { | |
if (user.isEmpty && password.isEmpty) { | |
self.logger.error("Attempt to start a new session without providing User and Password") | |
throw SCPSessionManagerError.EmptyCredentials | |
} else { | |
self.userName = user | |
self.password = password | |
// Create a new URLRequest and attach it to SAPURLSession | |
// Security parameters will be added if the session receive an authorization challenge | |
// which will inform "self" via SAPURLSession delegation | |
var urlRequest: URLRequest = URLRequest(url: self.appUrl!) | |
urlRequest.httpMethod = SAPURLSession.HTTPMethod.get | |
// Ask the delegate to provide the Application Connection ID | |
self.sapUrlSession?.sessionDescription = "X-SMP-APPCID Check" | |
let appCid = self.delegate?.session(self, applicationConnectionIdFor: self.sapUrlSession!) | |
// X-SMP-APPCID must be provided at least | |
if (appCid?.isEmpty)! { | |
self.logger.error("Attempt to start a new session without Application Connection ID") | |
throw SCPSessionManagerError.EmptyCredentials | |
} else { | |
urlRequest.setValue(appCid!, forHTTPHeaderField: "X-SMP-APPCID") | |
} | |
let dataTask = sapUrlSession?.dataTask(with: urlRequest) | |
{ | |
(data, response, error) in | |
// Check that did not receive errors | |
guard error == nil else { | |
self.delegate?.session(self, checkDidFailWithResponse: response!, withError: error!) | |
return | |
} | |
// Now check what we got: | |
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { | |
switch httpStatus.statusCode{ | |
case SAPURLSession.HTTPStatusCode.notFound: self.delegate?.session(self, checkDidFailWithResponse: response!, withError: SCPSessionManagerError.EndpointNotFound) | |
case SAPURLSession.HTTPStatusCode.internalServerError, 503: self.delegate?.session(self, checkDidFailWithResponse: response!, withError: SCPSessionManagerError.ServiceUnavailable) | |
default: self.delegate?.session(self, checkDidFailWithResponse: response!, withError: SCPSessionManagerError.UnknownNetworkError) | |
} | |
} else { | |
self.delegate?.session(self, checkDidSuccessWithResponse: response!) | |
} | |
} | |
dataTask?.resume( ) | |
} | |
} | |
// On new device registration required: | |
public func registerApplication( ) throws { | |
// Assume that startSession has been called: | |
var urlRequest: URLRequest = URLRequest(url: self.registrationUrl!) | |
urlRequest.httpMethod = SAPURLSession.HTTPMethod.post | |
urlRequest.setValue("application/json", forHTTPHeaderField: "Accept") | |
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") | |
let dataTask = sapUrlSession?.dataTask(with: urlRequest) | |
{ | |
(data, response, error) in | |
guard error != nil else { | |
self.logger.error("Got error while trying to register the device: \(error!)") | |
self.delegate?.session(self, registerDidFailWithResponse: response!, withData: data!, withError: error!) | |
return | |
} | |
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != SAPURLSession.HTTPStatusCode.created { | |
// Log the error first: | |
self.logger.error("Registration denied with status code \(httpResponse.statusCode)") | |
// Let the delegate decide: | |
switch httpResponse.statusCode{ | |
case SAPURLSession.HTTPStatusCode.unauthorized, SAPURLSession.HTTPStatusCode.forbidden: | |
self.delegate?.session(self, registerDidFailWithResponse: response!, withData: data!, withError: SCPSessionManagerError.InvalidCredentials) | |
case SAPURLSession.HTTPStatusCode.notFound: | |
self.delegate?.session(self, registerDidFailWithResponse: response!, withData: data!, withError: SCPSessionManagerError.EndpointNotFound) | |
default: self.delegate?.session(self, registerDidFailWithResponse: response!, withData: data!, withError: SCPSessionManagerError.UnknownNetworkError) | |
} | |
} else { | |
// Retrieve Application Connection ID | |
let json = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) | |
guard let root = json as? [String: Any], let d = root["d"] as? [String:Any], let appCid = d["ApplicationConnectionId"] as? String else { | |
self.logger.error("Unable to find Application Connection ID in response payload") | |
// Inform the delegate | |
self.delegate?.session(self, registerDidFailWithResponse: response!, withData: data!, withError: SCPSessionManagerError.ApplicationConnectionIdNotInPayload) | |
return | |
} | |
// It's a success: | |
self.logger.log("Registered successfully with ID \(appCid)") | |
self.delegate?.session(self, registerDidSuccessWithId: appCid) | |
} | |
} | |
dataTask?.resume() | |
} | |
// Returns an instance of OnlineDataProvider, which is attached to the current SAP URL Session | |
func createODataProvider(withName name:String ) -> OnlineODataProvider { | |
return OnlineODataProvider(serviceName: name, serviceRoot: self.appUrl!, sapURLSession: self.sapUrlSession!) | |
} | |
// MARK: SAPUrlSession Delegate Methods | |
// generated: SAP SCP SDK for iOS Assistant | |
func sapURLSession(_ session: SAPURLSession, task: SAPURLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping(SAPURLSession.AuthChallengeDisposition) -> Void) { | |
if challenge.previousFailureCount > 1 { | |
completionHandler(.performDefaultHandling) | |
return | |
} | |
// Note: This automatic server trust is for only testing purposes. | |
// The intented use is to install certificate to the device. Do not use it productively! | |
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { | |
let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!) | |
completionHandler(.use(credential)) | |
} else { | |
let credential = URLCredential(user: self.userName, password: self.password, persistence: .forSession) | |
completionHandler(.use(credential)) | |
} | |
} | |
// MARK: Utilities | |
fileprivate func createBasicAuthorizationString( ) -> String { | |
let loginString = String(format: "%@:%@", self.userName, self.password) | |
let loginData = loginString.data(using: String.Encoding.utf8)! | |
return loginData.base64EncodedString() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
SCPSessionManager.swift
Wrapper for mostly used functions required to communicate with the SAP Cloud Platform
This class is intended to be reused for applications that need to communicate with the SAP Cloud Platform Mobiles Services.
It includes basic functions which are mostly used in applications that make use of SCPms to access backend data via OData protocols, like checking of the application registration id (X-SMP-APPCID header) and the registration of new application ids.
The class provides access to SAPURLSession object, to wrap network communications in a single shared session and provides handler methods to react to issues or acknowledgements via delegation.
Usage
The class assumes that the startSession function is always called: the startSession handles the initialization of the network session and registers delegation for SAPURLSession object.
func startSession
This function starts the communication and attaches to SCPms: it requires a username and a password, than it calls the delegate object to fetch any existing Application Connection ID (X-SMP-APPCID). If this is provided, it is attached to the first request to check its validity: if the appCid is not valid anymore, a catchable exception is thrown to make the caller able to react and - for example - request a new Application Registration ID.
If case of acknowledgment, credentials are stored in the SAPURLSession for further usage.
func registerApplication
This function must be used everytime a new Application Connection ID is required.
The communication and the response parsing is done internally: if errors occur, a catchable exception is thrown. Otherwise, a string representing the new Application Connection ID is returned via delegation.
It can be used as a fallback method in case startSession function ends with a registration error (X-SMP-APPCID missing or invalid).
func createODataProvider
Creates a new instance of OnlineODataProvider object, registered with own SAPURLSession.
The provider can be used to instantiate an DataService implementation class (like the ones generated by the SAP CP SDK for iOS Assistant).
SAPURLSession Delegate
The class acts as a delegate of SAPURLSession object, therefore is called whenever the session needs intervention.
One example is delegate method didReceive challenge: which is called from the session if credentials are missing or not valid: the class responds by providing the username and the password registered in startSession function, so that the caller doesn't need to take care.