Skip to content

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

@jthomas

Description

@jthomas

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])
        }
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions