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

v4 Refactor Notice #653

Merged
merged 32 commits into from
Jan 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2d05950
start and layout of refactored code base
daltoniam Jan 24, 2019
f8238c4
proper offset
daltoniam Jan 25, 2019
7bb64ba
writing queue added. Compression stubbed out. Framing refactor is done
daltoniam Feb 4, 2019
c639b64
HTTP connetion work
daltoniam Feb 5, 2019
12e668f
foundation HTTP request creation
daltoniam Feb 8, 2019
5f46726
got compression hooked up
daltoniam Feb 8, 2019
75236d6
fixed conflicts
daltoniam Feb 8, 2019
5abcb7f
updated simple test
daltoniam Feb 9, 2019
db7685c
off by one
daltoniam Feb 9, 2019
63b76e1
finished up some of the TODOs
daltoniam Feb 9, 2019
0e6bdc8
start of security. SSL pinning next
daltoniam Mar 22, 2019
1cbfd81
swift 5 update
daltoniam Mar 27, 2019
21c38d3
changed simple test back
daltoniam Mar 27, 2019
ec0953e
tests are building again
daltoniam Mar 31, 2019
a664af6
updated travis for swift 5
daltoniam Mar 31, 2019
522b7e7
updated travis for swift 5 build
daltoniam Mar 31, 2019
d944b0d
package manager fix
daltoniam Mar 31, 2019
4993e49
disabled pod lint for now
daltoniam Mar 31, 2019
520e510
basic server support for unit tests
daltoniam Apr 4, 2019
ebcc043
added Network framework server
daltoniam Apr 5, 2019
c0d564f
Added Certificate Pinning Support
acmacalister Apr 27, 2019
b386d1e
Cleaned up docs, example project, licensing, etc
acmacalister Apr 27, 2019
c946f5c
laid the ground work for supporting both the native API and backwards…
daltoniam Jun 15, 2019
839bf25
disabled native websocket until offical release
daltoniam Aug 10, 2019
b8c4183
fixed conflicts
daltoniam Aug 10, 2019
6e3b96a
CI fixes by readding watchOS support of raw string parsing for HTTP h…
daltoniam Aug 26, 2019
136274d
WatchOS ifdef checks
daltoniam Aug 28, 2019
109dd03
fixed the build
daltoniam Sep 22, 2019
394afb9
README update and travis to xcode 11
daltoniam Sep 25, 2019
83ae006
fixed the unit tests
daltoniam Oct 2, 2019
0cea119
resolved conflicts
daltoniam Jan 2, 2020
e14338d
resolved conflicts
daltoniam Jan 2, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
osx_image: xcode10.2
osx_image: xcode11
language: objective-c
before_install:
- gem install cocoapods --pre
Expand Down
3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-nio-zlib-support.git", from: "1.0.0")
],
targets: [
.target(name: "Starscream")
.target(name: "Starscream",
path: "Sources")
]
)
254 changes: 56 additions & 198 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
![starscream](https://raw.githubusercontent.com/daltoniam/starscream/assets/starscream.jpg)

Starscream is a conforming WebSocket ([RFC 6455](http://tools.ietf.org/html/rfc6455)) client library in Swift.

Its Objective-C counterpart can be found here: [Jetfire](https://github.com/acmacalister/jetfire)
Starscream is a conforming WebSocket ([RFC 6455](http://tools.ietf.org/html/rfc6455)) library in Swift.

## Features

- Conforms to all of the base [Autobahn test suite](http://autobahn.ws/testsuite/).
- Nonblocking. Everything happens in the background, thanks to GCD.
- TLS/WSS support.
- Compression Extensions support ([RFC 7692](https://tools.ietf.org/html/rfc7692))
- Simple concise codebase at just a few hundred LOC.

## Example
### Import the framework

First thing is to import the framework. See the Installation instructions on how to add the framework to your project.

```swift
import Starscream
```

### Connect to the WebSocket Server

Once imported, you can open a connection to your WebSocket server. Note that `socket` is probably best as a property, so it doesn't get deallocated right after being setup.

```swift
Expand All @@ -28,86 +27,53 @@ socket.delegate = self
socket.connect()
```

After you are connected, there are some delegate methods that we need to implement.

### websocketDidConnect

websocketDidConnect is called as soon as the client connects to the server.

```swift
func websocketDidConnect(socket: WebSocketClient) {
print("websocket is connected")
}
```

### websocketDidDisconnect

websocketDidDisconnect is called as soon as the client is disconnected from the server.

```swift
func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {
print("websocket is disconnected: \(error?.localizedDescription)")
}
```

### websocketDidReceiveMessage

websocketDidReceiveMessage is called when the client gets a text frame from the connection.

```swift
func websocketDidReceiveMessage(socket: WebSocketClient, text: String) {
print("got some text: \(text)")
}
```

### websocketDidReceiveData

websocketDidReceiveData is called when the client gets a binary frame from the connection.

```swift
func websocketDidReceiveData(socket: WebSocketClient, data: Data) {
print("got some data: \(data.count)")
}
```

### Optional: websocketDidReceivePong *(required protocol: WebSocketPongDelegate)*

websocketDidReceivePong is called when the client gets a pong response from the connection. You need to implement the WebSocketPongDelegate protocol and set an additional delegate, eg: ` socket.pongDelegate = self`

```swift
func websocketDidReceivePong(socket: WebSocketClient, data: Data?) {
print("Got pong! Maybe some data: \(data?.count)")
After you are connected, there is either a delegate or closure you can use for process WebSocket events.

### Receiving data from a WebSocket

`didReceive` receives all the WebSocket events in a single easy to handle enum.

```swift
func didReceive(event: WebSocketEvent, client: WebSocket) {
switch event {
case .connected(let headers):
isConnected = true
print("websocket is connected: \(headers)")
case .disconnected(let reason, let code):
isConnected = false
print("websocket is disconnected: \(reason) with code: \(code)")
case .text(let string):
print("Received text: \(string)")
case .binary(let data):
print("Received data: \(data.count)")
case .ping(_):
break
case .pong(_):
break
case .viablityChanged(_):
break
case .reconnectSuggested(_):
break
case .cancelled:
isConnected = false
case .error(let error):
isConnected = false
handleError(error)
}
}
```

Or you can use closures.
The closure of this would be:

```swift
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!)
//websocketDidConnect
socket.onConnect = {
print("websocket is connected")
}
//websocketDidDisconnect
socket.onDisconnect = { (error: Error?) in
print("websocket is disconnected: \(error?.localizedDescription)")
socket.onEvent = { event in
switch event {
// handle events just like above...
}
}
//websocketDidReceiveMessage
socket.onText = { (text: String) in
print("got some text: \(text)")
}
//websocketDidReceiveData
socket.onData = { (data: Data) in
print("got some data: \(data.count)")
}
//you could do onPong as well.
socket.connect()
```

One more: you can listen to socket connection and disconnection via notifications. Starscream posts `WebsocketDidConnectNotification` and `WebsocketDidDisconnectNotification`. You can find an `Error` that caused the disconection by accessing `WebsocketDisconnectionErrorKeyName` on notification `userInfo`.


## The delegate methods give you a simple way to handle data from the server, but how do you send data?
### Writing to a WebSocket

### write a binary frame

Expand Down Expand Up @@ -160,90 +126,31 @@ The disconnect method does what you would expect and closes the socket.
socket.disconnect()
```

The socket can be forcefully closed, by specifying a timeout (in milliseconds). A timeout of zero will also close the socket immediately without waiting on the server.
The disconnect method can also send a custom close code if desired.

```swift
socket.disconnect(forceTimeout: 10, closeCode: CloseCode.normal.rawValue)
socket.disconnect(closeCode: CloseCode.normal.rawValue)
```

### isConnected
### Custom Headers, Protocols and Timeout

Returns if the socket is connected or not.

```swift
if socket.isConnected {
// do cool stuff.
}
```

### Custom Headers

You can also override the default websocket headers with your own custom ones like so:
You can override the default websocket headers, add your own custom ones and set a timeout:

```swift
var request = URLRequest(url: URL(string: "ws://localhost:8080/")!)
request.timeoutInterval = 5
request.timeoutInterval = 5 // Sets the timeout for the connection
request.setValue("someother protocols", forHTTPHeaderField: "Sec-WebSocket-Protocol")
request.setValue("14", forHTTPHeaderField: "Sec-WebSocket-Version")
request.setValue("chat,superchat", forHTTPHeaderField: "Sec-WebSocket-Protocol")
request.setValue("Everything is Awesome!", forHTTPHeaderField: "My-Awesome-Header")
let socket = WebSocket(request: request)
```

### Custom HTTP Method

Your server may use a different HTTP method when connecting to the websocket:

```swift
var request = URLRequest(url: URL(string: "ws://localhost:8080/")!)
request.httpMethod = "POST"
request.timeoutInterval = 5
let socket = WebSocket(request: request)
```

### Protocols

If you need to specify a protocol, simple add it to the init:

```swift
//chat and superchat are the example protocols here
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])
socket.delegate = self
socket.connect()
```

### Self Signed SSL

```swift
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])

//set this if you want to ignore SSL cert validation, so a self signed SSL certificate can be used.
socket.disableSSLCertValidation = true
```

### SSL Pinning

SSL Pinning is also supported in Starscream.

```swift
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])
let data = ... //load your certificate from disk
socket.security = SSLSecurity(certs: [SSLCert(data: data)], usePublicKeys: true)
//socket.security = SSLSecurity() //uses the .cer files in your app's bundle
```
You load either a `Data` blob of your certificate or you can use a `SecKeyRef` if you have a public key you want to use. The `usePublicKeys` bool is whether to use the certificates for validation or the public keys. The public keys will be extracted from the certificates automatically if `usePublicKeys` is choosen.

### SSL Cipher Suites

To use an SSL encrypted connection, you need to tell Starscream about the cipher suites your server supports.

```swift
socket = WebSocket(url: URL(string: "wss://localhost:8080/")!, protocols: ["chat","superchat"])

// Set enabled cipher suites to AES 256 and AES 128
socket.enabledSSLCipherSuites = [TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256]
```

If you don't know which cipher suites are supported by your server, you can try pointing [SSL Labs](https://www.ssllabs.com/ssltest/) at it and checking the results.
TODO: Update docs on how to load certificates and public keys into an app bundle, use the builtin pinner and TrustKit.

### Compression Extensions

Expand Down Expand Up @@ -272,7 +179,7 @@ Check out the SimpleTest project in the examples directory to see how to setup a

## Requirements

Starscream works with iOS 7/OSX 10.9 or above. It is recommended to use iOS 8/10.10 or above for CocoaPods/framework support. To use Starscream with a project targeting iOS 7, you must include all Swift files directly in your project.
Starscream works with iOS 8/10.10 or above for CocoaPods/framework support. To use Starscream with a project targeting iOS 7, you must include all Swift files directly in your project.

## Installation

Expand All @@ -286,7 +193,7 @@ To use Starscream in your project add the following 'Podfile' to your project
platform :ios, '9.0'
use_frameworks!

pod 'Starscream', '~> 3.0.2'
pod 'Starscream', '~> 4.0.0'

Then run:

Expand All @@ -308,7 +215,7 @@ $ brew install carthage
To integrate Starscream into your Xcode project using Carthage, specify it in your `Cartfile`:

```
github "daltoniam/Starscream" >= 3.0.2
github "daltoniam/Starscream" >= 4.0.0
```

### Accio
Expand All @@ -318,7 +225,7 @@ Check out the [Accio](https://github.com/JamitLabs/Accio) docs on how to add a i
Add the following to your Package.swift:

```swift
.package(url: "https://github.com/daltoniam/Starscream.git", .upToNextMajor(from: "3.1.0")),
.package(url: "https://github.com/daltoniam/Starscream.git", .upToNextMajor(from: "4.0.0")),
```

Next, add `Starscream` to your App targets dependencies like so:
Expand Down Expand Up @@ -354,7 +261,7 @@ Once you have your Swift package set up, adding Starscream as a dependency is as

```swift
dependencies: [
.Package(url: "https://github.com/daltoniam/Starscream.git", majorVersion: 3)
.Package(url: "https://github.com/daltoniam/Starscream.git", majorVersion: 4)
]
```

Expand All @@ -368,58 +275,9 @@ Add the `Starscream.xcodeproj` to your Xcode project. Once that is complete, in

If you are running this in an OSX app or on a physical iOS device you will need to make sure you add the `Starscream.framework` to be included in your app bundle. To do this, in Xcode, navigate to the target configuration window by clicking on the blue project icon, and selecting the application target under the "Targets" heading in the sidebar. In the tab bar at the top of that window, open the "Build Phases" panel. Expand the "Link Binary with Libraries" group, and add `Starscream.framework`. Click on the + button at the top left of the panel and select "New Copy Files Phase". Rename this new phase to "Copy Frameworks", set the "Destination" to "Frameworks", and add `Starscream.framework` respectively.


## WebSocketAdvancedDelegate
The advanced delegate acts just like the simpler delegate but provides some additional information on the connection and incoming frames.

```swift
socket.advancedDelegate = self
```

In most cases you do not need the extra info and should use the normal delegate.

#### websocketDidReceiveMessage
```swift
func websocketDidReceiveMessage(socket: WebSocketClient, text: String, response: WebSocket.WSResponse) {
print("got some text: \(text)")
print("First frame for this message arrived on \(response.firstFrame)")
}
```

#### websocketDidReceiveData
```swift
func websocketDidReceiveData(socket: WebSocketClient, data: Date, response: WebSocket.WSResponse) {
print("got some data it long: \(data.count)")
print("A total of \(response.frameCount) frames were used to send this data")
}
```

#### websocketHttpUpgrade
These methods are called when the HTTP upgrade request is sent and when the response returns.
```swift
func websocketHttpUpgrade(socket: WebSocketClient, request: CFHTTPMessage) {
print("the http request was sent we can check the raw http if we need to")
}
```

```swift
func websocketHttpUpgrade(socket: WebSocketClient, response: CFHTTPMessage) {
print("the http response has returned.")
}
```

## Swift versions

* Swift 4.2 - 3.0.6

## KNOWN ISSUES
- WatchOS does not have the the CFNetwork String constants to modify the stream's SSL behavior. It will be the default Foundation SSL behavior. This means watchOS CANNOT use `SSLCiphers`, `disableSSLCertValidation`, or SSL pinning. All these values set on watchOS will do nothing.
- Linux does not have the security framework, so it CANNOT use SSL pinning or `SSLCiphers` either.


## TODOs

- [ ] Add Unit Tests - Local WebSocket server that runs against Autobahn
- [ ] Proxy support

## License

Expand Down