Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift SDK does not support self-signed server certificates. #12

Open
jthomas opened this issue Feb 2, 2018 · 5 comments
Open

Swift SDK does not support self-signed server certificates. #12

jthomas opened this issue Feb 2, 2018 · 5 comments

Comments

@jthomas
Copy link
Member

jthomas commented Feb 2, 2018

Local instances of the platform use a self-signed SSL certificate. The Swift SDK (_Whisk.swift) fails when invoked against platform endpoints without a valid SSL certificate.

The JavaScript SDK supports a constructor argument to turn off certificat checking to resolve this issue. Looking into implementing this behaviour for the Swift SDK, I have discovered a blocking issue due to the lack of support in the open-source Swift Foundation libraries.

In Swift, certificate checking can be turned off by creating a new URLSessionDelegate with always trusts the server.

class SessionDelegate:NSObject, URLSessionDelegate
{
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
        completionHandler(URLSession.AuthChallengeDisposition.useCredential, credential)
    }
}

This can be used when creating the URLSession to use based upon a method parameter.

let session = ignoreCerts ? URLSession(configuration: .default, delegate: SessionDelegate(), delegateQueue: nil) : URLSession(configuration: URLSessionConfiguration.default)

This works on OS X but compiling the code on Linux, I ran into the following issue.

_Whisky.swift:25:39: error: incorrect argument label in call (have 'trust:', expected 'coder:')
        let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
                                      ^~~~~~
                                       coder
root@3a4fde570648:/source# swift -v
Swift version 4.0 (swift-4.0-RELEASE)
Target: x86_64-unknown-linux-gnu

Looking into the source code for the Swift foundation core libraries, I discovered this method has not been implemented.
https://github.com/apple/swift-corelibs-foundation/blob/master/Foundation/URLCredential.swift#L155

// TODO: We have no implementation for Security.framework primitive types SecIdentity and SecTrust yet

Talking to the IBM@Swift team, support for this feature is being worked on but won't be available until Swift 5 at the earliest.

Until then, we will have to document the behaviour and wait for the foundation libraries to catch up. I've attached the completed _Whisk.swift demonstrating the bug.

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import Foundation
import Dispatch


class SessionDelegate:NSObject, URLSessionDelegate
{
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
        completionHandler(URLSession.AuthChallengeDisposition.useCredential, credential)
    }
}

class Whisk {
    
    static var baseUrl = ProcessInfo.processInfo.environment["__OW_API_HOST"]
    static var apiKey = ProcessInfo.processInfo.environment["__OW_API_KEY"]
    
    class func invoke(actionNamed action : String, withParameters params : [String:Any], ignoreCerts: Bool = false, blocking: Bool = true) -> [String:Any] {
        let parsedAction = parseQualifiedName(name: action)
        let strBlocking = blocking ? "true" : "false"
        let path = "/api/v1/namespaces/\(parsedAction.namespace)/actions/\(parsedAction.name)?blocking=\(strBlocking)"
        
        return sendWhiskRequestSyncronish(uriPath: path, params: params, method: "POST", ignoreCerts: ignoreCerts)
    }
    
    class func trigger(eventNamed event : String, ignoreCerts: Bool = false, withParameters params : [String:Any]) -> [String:Any] {
        let parsedEvent = parseQualifiedName(name: event)
        let path = "/api/v1/namespaces/\(parsedEvent.namespace)/triggers/\(parsedEvent.name)?blocking=true"
        
        return sendWhiskRequestSyncronish(uriPath: path, params: params, method: "POST", ignoreCerts: ignoreCerts)
    }
    
    class func createTrigger(triggerNamed trigger: String, ignoreCerts: Bool = false, withParameters params : [String:Any]) -> [String:Any] {
        let parsedTrigger = parseQualifiedName(name: trigger)
        let path = "/api/v1/namespaces/\(parsedTrigger.namespace)/triggers/\(parsedTrigger.name)"
        return sendWhiskRequestSyncronish(uriPath: path, params: params, method: "PUT", ignoreCerts: ignoreCerts)
    }
    
    class func createRule(ruleNamed ruleName: String, withTrigger triggerName: String, andAction actionName: String, ignoreCerts: Bool = false) -> [String:Any] {
        let parsedRule = parseQualifiedName(name: ruleName)
        let path = "/api/v1/namespaces/\(parsedRule.namespace)/rules/\(parsedRule.name)"
        let params = ["trigger":triggerName, "action":actionName]
        return sendWhiskRequestSyncronish(uriPath: path, params: params, method: "PUT", ignoreCerts: ignoreCerts)
    }
    
    // handle the GCD dance to make the post async, but then obtain/return
    // the result from this function sync
    private class func sendWhiskRequestSyncronish(uriPath path: String, params : [String:Any], method: String, ignoreCerts: Bool) -> [String:Any] {
        var response : [String:Any]!
        
        let queue = DispatchQueue.global()
        let invokeGroup = DispatchGroup()
        
        invokeGroup.enter()
        queue.async {
            postUrlSession(uriPath: path, ignoreCerts: ignoreCerts, params: params, method: method, group: invokeGroup) { result in
                response = result
            }
        }
        
        // On one hand, FOREVER seems like an awfully long time...
        // But on the other hand, I think we can rely on the system to kill this
        // if it exceeds a reasonable execution time.
        switch invokeGroup.wait(timeout: DispatchTime.distantFuture) {
        case DispatchTimeoutResult.success:
            break
        case DispatchTimeoutResult.timedOut:
            break
        }
        
        return response
    }
    
    
    /**
     * Using new UrlSession
     */
    private class func postUrlSession(uriPath: String, ignoreCerts: Bool, params : [String:Any], method: String,group: DispatchGroup, callback : @escaping([String:Any]) -> Void) {
        
        guard let encodedPath = uriPath.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed) else {
            callback(["error": "Error encoding uri path to make openwhisk REST call."])
            return
        }
        
        let urlStr = "\(baseUrl!)\(encodedPath)"
        if let url = URL(string: urlStr) {
            var request = URLRequest(url: url)
            request.httpMethod = method
            
            do {
                request.addValue("application/json", forHTTPHeaderField: "Content-Type")
                request.httpBody = try JSONSerialization.data(withJSONObject: params)
                
                let loginData: Data = apiKey!.data(using: String.Encoding.utf8, allowLossyConversion: false)!
                let base64EncodedAuthKey  = loginData.base64EncodedString(options: NSData.Base64EncodingOptions(rawValue: 0))
                request.addValue("Basic \(base64EncodedAuthKey)", forHTTPHeaderField: "Authorization")
                let session = ignoreCerts ? URLSession(configuration: .default, delegate: SessionDelegate(), delegateQueue: nil) : URLSession(configuration: URLSessionConfiguration.default)
                
                let task = session.dataTask(with: request, completionHandler: {data, response, error -> Void in
                    
                    // exit group after we are done
                    defer {
                        group.leave()
                    }
                    
                    if let error = error {
                        callback(["error":error.localizedDescription])
                    } else {
                        
                        if let data = data {
                            do {
                                //let outputStr  = String(data: data, encoding: String.Encoding.utf8) as String!
                                //print(outputStr)
                                let respJson = try JSONSerialization.jsonObject(with: data)
                                if respJson is [String:Any] {
                                    callback(respJson as! [String:Any])
                                } else {
                                    callback(["error":" response from server is not a dictionary"])
                                }
                            } catch {
                                callback(["error":"Error creating json from response: \(error)"])
                            }
                        }
                    }
                })
                
                task.resume()
            } catch {
                callback(["error":"Got error creating params body: \(error)"])
            }
        }
    }
    
    // separate an OpenWhisk qualified name (e.g. "/whisk.system/samples/date")
    // into namespace and name components
    private class func parseQualifiedName(name qualifiedName : String) -> (namespace : String, name : String) {
        let defaultNamespace = "_"
        let delimiter = "/"
        
        let segments :[String] = qualifiedName.components(separatedBy: delimiter)
        
        if segments.count > 2 {
            return (segments[1], Array(segments[2..<segments.count]).joined(separator: delimiter))
        } else if segments.count == 2 {
            // case "/action" or "package/action"
            let name = qualifiedName.hasPrefix(delimiter) ? segments[1] : segments.joined(separator: delimiter)
            return (defaultNamespace, name)
        } else {
            return (defaultNamespace, segments[0])
        }
    }
}
@csantanapr
Copy link
Member

csantanapr commented Feb 2, 2018

Thanks for opening this.
I thought I already opened an issue to document my findings.

For swift 4 we plan to use urlSession.
Ignoring SSL is not a good thing, so the library should not be supporting this.

I made the SDK to allow to take an override to APIHOST, by allowing the user to specify a baseURL.
In this baseURL user can specify the Whisk API using http instead of https

This allowed me to write test to pass in Travis.

I production system WHISK APIHOST will always be https with valid certificates.
I opened issue #11 and will plan to work on it.

We can leave this issue open until linux supports self sign certs, and then allow an override in the Class to skip ssl checks, but should not be the default.

@csantanapr
Copy link
Member

Here is an example

 //Overriding WHISK API HOST using baseUrl, only applicable in testing with self sign ssl certs"
 Whisk.baseUrl = "http://172.17.0.1:10001"
 let invokeResult = Whisk.invoke(actionNamed: "/whisk.system/utils/date", withParameters: [:], blocking: false)

@jthomas
Copy link
Member Author

jthomas commented Feb 3, 2018

In the code above, this won't be the default behaviour. It relies on the user explictly setting a function parameter, which defaults to false. This mirrors the behaviour of the JavaScript SDK.

@csantanapr
Copy link
Member

Yep that correct it would no be the default.
Default behavior is that user coding the action doesn’t have to worry the environment variable with APIHOST will be use they don’t have to specify baseURL

@csantanapr
Copy link
Member

csantanapr commented Feb 5, 2018

@jthomas

In the code above, this won't be the default behaviour. It relies on the user explictly setting a function parameter, which defaults to false. This mirrors the behaviour of the JavaScript SDK.

Sorry read your comment again, I think I missed it the first time.

The default behavior for what ever reason today (ie. 3.1.1) _Whisk class API is blocking: Bool = true different from the JavaScript SDK.
https://github.com/apache/incubator-openwhisk-runtime-swift/blob/master/core/swift3.1.1Action/spm-build/_Whisk.swift#L23

 class func invoke(actionNamed action : String, withParameters params : [String:Any], blocking: Bool = true) -> [String:Any] {

It would be good to have consistency with theJavaScript SDK, but I think is more important to not break people's code by changing the API when they move from swift 3 to swift4.

I think it's something to note when we get to building new Library/SDK for swift4+ both client and server, and maybe a new Class OpenWhisk, then I think will have an opportunity to change the API to invoke

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants