GithubHelp home page GithubHelp logo

iscriptology / swamp Goto Github PK

View Code? Open in Web Editor NEW
42.0 8.0 36.0 522 KB

the WAMP WebSocket subprotocol implemented purely in Swift using Starscream & SwiftyJSON

License: MIT License

Ruby 0.33% Swift 87.11% Objective-C 9.11% Shell 3.34% Python 0.11%

swamp's Introduction

Version License Platform Swift Package Manager compatible

Swamp - Swift WAMP implementation

Swamp is a WAMP implementation in Swift.

It currently supports calling remote procedures, subscribing on topics, and publishing events. It also supports authentication using ticket & wampcra authentication.

Swamp utilizes WebSockets as its only available transport, and JSON as its serialization method.

Contributions to support MessagePack & Raw Sockets will be merged gladly!

Swift Versions

Swift Version Swamp Version Requirements
2.x 0.1.x OSX 10.9 or iOS 8.0
3 0.2.0 and above OSX 10.10 or iOS 8.0

Installation

cocoapods

To use Swamp through cocoapods, add

pod 'Swamp', '~> 0.2.0'

to your Podfile. (use '~> 0.1.0' for Swift 2)

Swift Package Manager

To use Swamp through Swift Package Manager, create a Package.swift file:

import PackageDescription

let package = Package(
    name: "SwampTestProject",
    targets: [],
    dependencies: [
        .Package(url: "https://github.com/RadarBee/swamp.git", majorVersion: 0, minor: 2)
    ]
)

$ swift build

Usage

Connect to router

import Swamp

let swampTransport = WebSocketSwampTransport(wsEndpoint:  NSURL(string: "ws://my-router.com:8080/ws")!)
let swampSession = SwampSession(realm: "router-defined-realm", transport: swampTransport)
// Set delegate for callbacks
// swampSession.delegate = <SwampSessionDelegate implementation>
swampSession.connect()
swampSession.disconnect()
SwampSession constructor parameters
  • realm - which realm to join
  • transport - a SwampTransport implementation
  • authmethods authid authrole authextra - See your router's documentation and use accordingly
Connection/Disconnection
  • connect() - Establish transport and perform authentication if configured.
  • disconnect() - Opposite.

Now you should wait for your delegate's callbacks:

SwampSessionDelegate interface

Implement the following methods:

  • func swampSessionHandleChallenge(authMethod: String, extra: [String: AnyObject]) -> String
    • Fired when a challenge request arrives.
    • You can return SwampCraAuthHelper.sign("your-secret", extra["challenge"] as! String) to support wampcra auth method.
  • func swampSessionConnected(session: SwampSession, sessionId: Int)
  • Fired once the session has established and authenticated a session, and has joined the realm successfully. (AKA You may now call, subscribe & publish.)
  • func swampSessionEnded(reason: String)
  • Fired once the connection has ended.
  • reason is usually a WAMP-domain error, but it can also be a textual description of WTF just happened

Let's get the shit started!

  • General note: Lots of callback functions receive args-kwargs pairs, check your other client implementaion to see which of them is utilized, and act accordingly.
Calling remote procedures

Calling may fire two callbacks:

  • onSuccess - if calling has completed without errors.
  • onError - If the call has failed. (Either in router or in peer client.)
Signature
public func call(proc: String, options: [String: AnyObject]=[:], args: [AnyObject]?=nil, kwargs: [String: AnyObject]?=nil, onSuccess: CallCallback, onError: ErrorCallCallback)
Simple use case:
session.call("wamp.procedure", args: [1, "argument1"],
    onSuccess: { details, results, kwResults in
        // Usually result is in results[0], but do a manual check in your infrastructure
    },
    onError: { details, error, args, kwargs in
        // Handle your error here (You can ignore args kwargs in most cases)
    })
Full use case:
session.call("wamp.procedure", options: ["disclose_me": true], args: [1, "argument1"], kwargs: ["arg1": 1, "arg2": "argument2"], 
    onSuccess: { details, results, kwResults in
        // Usually result is in results[0], but do a manual check in your infrastructure
    },
    onError: { details, error, args, kwargs in
        // Handle your error here (You can ignore args kwargs in most cases)
    })
Subscribing on topics

Subscribing may fire three callbacks:

  • onSuccess - if subscription has succeeded.
  • onError - if it has not.
  • onEvent - if it succeeded, this is fired when the actual event was published.
Signature
public func subscribe(topic: String, options: [String: AnyObject]=[:], onSuccess: SubscribeCallback, onError: ErrorSubscribeCallback, onEvent: EventCallback)
Simple use case:
session.subscribe("wamp.topic", onSuccess: { subscription in 
    // subscription can be stored for subscription.cancel()
    }, onError: { details, error in
                                
    }, onEvent: { details, results, kwResults in
        // Event data is usually in results, but manually check blabla yadayada
    })
Full use case:
session.subscribe("wamp.topic", options: ["disclose_me": true], 
    onSuccess: { subscription in 
        // subscription can be stored for subscription.cancel()
    }, onError: { details, error in
        // handle error                        
    }, onEvent: { details, results, kwResults in
        // Event data is usually in results, but manually check blabla yadayada
    })
Publishing events

Publishing may either be called without callbacks (AKA unacknowledged) or with the following two callbacks:

  • onSuccess - if publishing has succeeded.
  • onError - if it has not.
Signature
// without acknowledging
public func publish(topic: String, options: [String: AnyObject]=[:], args: [AnyObject]?=nil, kwargs: [String: AnyObject]?=nil)
// with acknowledging
public func publish(topic: String, options: [String: AnyObject]=[:], args: [AnyObject]?=nil, kwargs: [String: AnyObject]?=nil, onSuccess: PublishCallback, onError: ErrorPublishCallback) {
Simple use case:
session.publish("wamp.topic", args: [1, "argument2"])
Full use case:
session.publish("wamp.topic", options: ["disclose_me": true],  args: [1, "argument2"], kwargs: ["arg1": 1, "arg2": "argument2"],
    onSuccess: {
        // Publication has been published!
    }, onError: { details, error in
        // Handle error (What can it be except wamp.error.not_authorized?)
    })

Testing

For now, only integration tests against crossbar exist. I plan to add unit tests in the future.

In order to run the tests:

  1. Install Docker for Mac (Easy Peasy)
  2. Open Example/Swamp.xcworkspace with XCode
  3. Select Swamp_Test-iOS or Swamp_Test-OSX
  4. Run the tests! (Product -> Test or โŒ˜U)

Troubleshooting

If for some reason the tests fail, make sure:

  • You have docker installed and available at /usr/local/bin/docker
  • You have an available port 8080 on your machine

You can also inspect Example/swamp-crossbar-instance.log to find out what happened with the crossbar instance while the tests were executing.

Roadmap

  1. MessagePack & Raw Sockets
  2. Callee role
  3. More robust codebase and error handling
  4. More generic and comfortable API
  5. Advanced profile features

Contributions

  • Yossi Abraham, [email protected] (Author)
  • Dany Sousa, @danysousa (Swift 3 support
  • Kevin Lanik, @MPKevin (Swift Package Manager support)

License

I don't care, MIT because it's pod lib create default and I'm too lazy to tldrlegal.

swamp's People

Contributors

danysousa avatar mpkevin avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

swamp's Issues

status of swift 3 / carthage support

I'm curious if you are already working on migrating to Swift 3, have plans to do so, or would accept a PR for same.

Ditto for Carthage support.

WatchOS support

Hello,

Thanks for such a wonderful library. Can you update your pod specification to provide support for watch os too? Thanks.

how to serialize JSON objects across WAMP

It took me a while to figure out how to get this working nicely, so I thought I'd share some tips. If you want to easily serialize or ingest JSON objects through the WAMP connection the serializer can make it hard to accomplish. Many of the available frameworks want to operate directly on JSON data, or spit it out as JSON, but swamp only exposes Arrays/Dictionaries.

Fortunately I found Wrap and Unbox. Wrap is an object mapper that translates class properties into a Dictionary that can be passed to SwiftyJSON. Unbox is used with JSON, but can also accept a Dictionary to rehydrate your classes. Both handle optionals and dates and custom behaviors.

Step 1: Wrap your objects. You can do this when you are building arguments for your RPC calls, or you can do it within swamp using the following technique:

// declare a protocol or class your objects will conform to
class WampMessage: Unboxable {
}

// override _JSONSwampSerializer_ to look for your custom class/protocol
class WampMessageSwampSerializer: JSONSwampSerializer {

    open func packArray(_ arrayData:[Any]) -> [JSON] {        
        var jsonArray = [JSON]()
          for item in arrayData {
            
            if let arrayItem = item as? Array<Any> {
                let newArray = packArray(arrayItem)
                jsonArray.append(JSON(newArray))
            } else {
                do {
                    if let wampItem = item as? WampMessage {
                        let dict: [String:Any] = try wrap(wampItem)
                        jsonArray.append(JSON(dict))
                    } else {
                        jsonArray.append(JSON(item))
                    }
                } catch {
                    print("Fatal exception while packing `\(item)` into JSON")
                }
            }
        }
        return jsonArray
    }

    override func pack(_ data: [Any]) -> Data? {        
        let json = JSON(packArray(data))
        do {
            return try json.rawData()
        }
        catch let err as NSError {
            print("error = \(err.localizedDescription)")
            return nil
        }
    }
}

Step 2: Unbox the results. This example shows a WampMessage being passed in (see Step 1), but you could just as easily use bare JSON or Swift types, or convert the object inline by doing something like: let arg3: [String:Any] = try wrap(MyWampMessageObject() )

In the results block it shows a new object being initialized from the results using Unbox. If you didn't know the object type from the route being used, you might have to look at a value in the results first in order to determine what kind of object to Unbox

        let wampArgs: [Any] = [ arg1, arg2, MyWampMessageObject() ]
        swampSession.call("my.route", options: [:], args: wampArgs, kwargs: nil, onSuccess: { details, results, kwResults in
            if let results = results, let dict = results[0] as? [String:Any] {
                do {
                    let userInfo: MyWampMessageResult = try unbox(dictionary: dict)
                } catch {
                    print("failed to decode userInfo")
                }
            }
        }, onError: { details, error, args, kwargs in
            print ("ERROR: details: \(details), error: \(error), kwargs: \(kwargs)")
        })

updating swamp

@iscriptology I'm not sure the best way to proceed with this. I need to get SWAMP working quickly so I can move on to other things. I would like to be able to work with you and create a more robust implementation, but as I said in my PR for Carthage support, my patches are going to all depend on each other.

If you don't have spare time to work on this right now, I'll proceed with development in my own fork, and maybe at some point in the future we can work together to merge the two.

Here's my short term plan:
add enums for WAMP protocol and auth method
add separate WebSocketSwampTransport initializers for JSON and MsgPack
fix wampcra support for salted passwords
remove cryptoswift, replace with CommonCrypto (CryptoSwift is a very heavy dependency for one single call to HMAC)

I can't figure out if is possible to advertise multiple protocols [json, msgpack] and get enough information back via the websocket to set the mode and serializer properly, but it would be nice to have that happen programmatically.

Issuing RPC calls from multiple background threads clobbers the requestId

I have a scenario where I must issue multiple RPCs via swamp and then wait for all of them to complete before taking another step. I solved my use case with a DispatchGroup in manual mode (calling DispatchGroup.join() before issuing swampSession.call, calling DispatchGroup.leave() in the closures, and then using DispatchGroup.notify to wait for all issued calls to finish.

This mostly works fine when the DispatchGroup is managed from the main thread, or larded up with debug statements. But occasionally it hangs; not all of the RPC calls succeed so the group is never notified.

I tracked the problem down to the requestIds of the various RPC calls. Because swift variables are not atomic, the default implementation here is not thread safe. Multiple threads can increment the counter and then overwrite the result.

    fileprivate func generateRequestId() -> Int {
        self.currRequestId += 1
        return self.currRequestId
    }

This tweaked version uses an unfair lock to make the increment thread safe. It's pretty much the best option for Swift 3. Note that the stack variable is necessary otherwise another thread could increment the requestId after the lock is released, before it is returned.

    private var unfair_lock = os_unfair_lock_s()
    fileprivate func generateRequestId() -> Int {
        os_unfair_lock_lock(&unfair_lock)
        let newRequestId = self.currRequestId + 1
        self.currRequestId = newRequestId
        os_unfair_lock_unlock(&unfair_lock)
        return newRequestId
    }

I am not certain that there are no other threading issues, but this appears to have fixed my one issue nicely.

Does it support wss?

I am working on a project, which need to handle wss instead of ws. Do I need additional work for wss? Moreover, does Swamp 0.2.0 support WAMP 1.0 server?

Crashes sometimes when leaving view controller

I have a working WAMP implementation that crashes every 50 times or so. It happens when I leave the screen and the disconnect() method gets called:

class WampConnector: NSObject {
    static var authid = ""
    let wampEndpoint = "ws://wamp.workspace-beta.nl:9090"
    let realm = "workspace"
    let swampTransport: WebSocketSwampTransport
    let swampSession: SwampSession
    
    init?(withDelegate delegate: SwampSessionDelegate) {
        let url = URL(string: wampEndpoint) ?? URL(fileURLWithPath: "")
        
        swampTransport = WebSocketSwampTransport(wsEndpoint:  url)
        swampSession = SwampSession(realm: realm, transport: swampTransport, authmethods: ["ticket"])
        swampSession.delegate = delegate

        swampSession.connect()
        
        super.init()
    }
    
    func disconnect() {
        swampSession.disconnect("Leaving screen")
    }
}

The error occurs when force-unwrapping the serializer and/or data.

    fileprivate func sendMessage(_ message: SwampMessage){
        let marshalledMessage = message.marshal()
        let data = self.serializer!.pack(marshalledMessage as [Any])!
        self.transport.sendData(data)
    }

I notice force unwrapping is the default way to handle optionals in the library, probably to ensure errors get caught early instead of just returning after a guard statement and failing silently. But I think this error occurs because of some kind of race condition that's really hard to reproduce and it might be safer to guard let everything and return when the serializer is gone or no data instance is created.

Crash when subscribing or calling

Hi! I have server with rooms to chat. Full url is ws://93.171.XXX.XX:8080/conversation_realtime/sportspace581a1541590b21.14581077".
But I have to connect to this by 2 steps:

  1. connect to "ws://93.171.XXX.XX:8080"
  2. subscribe to room "conversation_realtime/sportspace581a1541590b21.14581077"

In JS it looks like:

var _WS_URI = "ws://93.171.242.80:8080";
var webSocket = WS.connect(_WS_URI);

webSocket.on("socket/connect", function(session){
session.subscribe("conversation_realtime/sportspace581a1541590b21.14581077", function(uri, payload){
                    console.log(payload.message.text);
}
}

When I try to call in my project swampSession.call("socket/connect"... or swampSession.subscribe("conversation_realtime/sportspace581a1541590b21.14581077".... my app crashes in SwampSession.swift with error "fatal error: unexpectedly found nil while unwrapping an Optional value" in this part:

fileprivate func sendMessage(_ message: SwampMessage){
       let marshalledMessage = message.marshal()
       let data = self.serializer!.pack(marshalledMessage as [Any])!
       self.transport.sendData(data)
   }

How can I realize it in your library? And what is "realm" in my case?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.