Skip to content

Instantly share code, notes, and snippets.

@robypag
Created May 16, 2017 12:28
Show Gist options
  • Save robypag/fcce9711c0fcfaa0e1c7ce9545dbc44f to your computer and use it in GitHub Desktop.
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
//
// 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()
}
}
@robypag
Copy link
Author

robypag commented May 16, 2017

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment