├── .ruby-version ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── general-question.md │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── release.yml ├── Gemfile ├── examples ├── WebSocketsOrgEcho │ ├── WebSocketsOrgEcho │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── URL+Extensions.swift │ │ ├── AppDelegate.swift │ │ ├── ViewController.swift │ │ ├── Info.plist │ │ └── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ ├── Pods │ │ ├── Target Support Files │ │ │ ├── Starscream │ │ │ │ ├── Starscream.modulemap │ │ │ │ ├── Starscream-dummy.m │ │ │ │ ├── Starscream-prefix.pch │ │ │ │ ├── Starscream-umbrella.h │ │ │ │ ├── Starscream.xcconfig │ │ │ │ └── Starscream-Info.plist │ │ │ └── Pods-WebSocketsOrgEcho │ │ │ │ ├── Pods-WebSocketsOrgEcho.modulemap │ │ │ │ ├── Pods-WebSocketsOrgEcho-dummy.m │ │ │ │ ├── Pods-WebSocketsOrgEcho-acknowledgements.plist │ │ │ │ ├── Pods-WebSocketsOrgEcho-umbrella.h │ │ │ │ ├── Pods-WebSocketsOrgEcho.debug.xcconfig │ │ │ │ ├── Pods-WebSocketsOrgEcho.release.xcconfig │ │ │ │ ├── Pods-WebSocketsOrgEcho-Info.plist │ │ │ │ └── Pods-WebSocketsOrgEcho-frameworks.sh │ │ ├── Manifest.lock │ │ └── Local Podspecs │ │ │ └── Starscream.podspec.json │ ├── WebSocketsOrgEcho.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Podfile │ └── Podfile.lock ├── AutobahnTest │ ├── Autobahn.xcodeproj │ │ └── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ ├── .gitignore │ └── Autobahn │ │ ├── Info.plist │ │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.xib │ │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ └── ViewController.swift └── SimpleTest │ ├── SimpleTest.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcuserdata │ │ │ └── dalton.xcuserdatad │ │ │ │ └── UserInterfaceState.xcuserstate │ │ └── xcshareddata │ │ │ └── SimpleTest.xccheckout │ └── xcuserdata │ │ └── dalton.xcuserdatad │ │ └── xcschemes │ │ ├── xcschememanagement.plist │ │ └── SimpleTest.xcscheme │ ├── README.md │ ├── .gitignore │ ├── ws-server.rb │ └── SimpleTest │ ├── Images.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── LaunchImage.launchimage │ │ └── Contents.json │ ├── Info.plist │ ├── AppDelegate.swift │ ├── Base.lproj │ └── Main.storyboard │ └── ViewController.swift ├── Sources ├── PrivacyInfo.xcprivacy ├── Info.plist ├── Compression │ ├── Compression.swift │ └── WSCompression.swift ├── Starscream.h ├── Engine │ ├── Engine.swift │ ├── NativeEngine.swift │ └── WSEngine.swift ├── Security │ ├── Security.swift │ └── FoundationSecurity.swift ├── Server │ ├── Server.swift │ └── WebSocketServer.swift ├── DataBytes │ └── Data+Extensions.swift ├── Transport │ ├── Transport.swift │ ├── TCPTransport.swift │ └── FoundationTransport.swift ├── Framer │ ├── FoundationHTTPServerHandler.swift │ ├── FrameCollector.swift │ ├── FoundationHTTPHandler.swift │ ├── StringHTTPHandler.swift │ └── HTTPHandler.swift └── Starscream │ └── WebSocket.swift ├── fastlane ├── Fastfile └── README.md ├── Tests ├── Info.plist ├── StarscreamTests │ └── StarscreamTests.swift ├── MockTransport.swift ├── CompressionTests.swift ├── MockServer.swift └── FuzzingTests.swift ├── Starscream.podspec ├── Package.swift ├── .gitignore ├── Starscream.xcodeproj └── xcshareddata │ └── xcschemes │ └── Starscream.xcscheme └── Gemfile.lock /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.2 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "fastlane" 4 | gem "cocoapods" 5 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/WebSocketsOrgEcho/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Target Support Files/Starscream/Starscream.modulemap: -------------------------------------------------------------------------------- 1 | framework module Starscream { 2 | umbrella header "Starscream-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Target Support Files/Starscream/Starscream-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Starscream : NSObject 3 | @end 4 | @implementation PodsDummy_Starscream 5 | @end 6 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Issue Link 🔗 2 | > Please attach the link to an issue if it exists. 3 | 4 | ### Goals ⚽ 5 | > What you hope to address within this PR. 6 | 7 | ### Implementation Details 🚧 8 | > Additional details about the PR. 9 | -------------------------------------------------------------------------------- /examples/AutobahnTest/Autobahn.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/SimpleTest/SimpleTest.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_WebSocketsOrgEcho { 2 | umbrella header "Pods-WebSocketsOrgEcho-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_WebSocketsOrgEcho : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_WebSocketsOrgEcho 5 | @end 6 | -------------------------------------------------------------------------------- /examples/SimpleTest/SimpleTest.xcodeproj/project.xcworkspace/xcuserdata/dalton.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/threema-ch/Starscream/master/examples/SimpleTest/SimpleTest.xcodeproj/project.xcworkspace/xcuserdata/dalton.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Target Support Files/Starscream/Starscream-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/WebSocketsOrgEcho.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/WebSocketsOrgEcho.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'WebSocketsOrgEcho' do 5 | # Comment the next line if you're not using Swift and don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for WebSocketsOrgEcho 9 | 10 | pod 'Starscream', :path => '../../' 11 | end 12 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Starscream (3.0.6) 3 | 4 | DEPENDENCIES: 5 | - Starscream (from `../../`) 6 | 7 | EXTERNAL SOURCES: 8 | Starscream: 9 | :path: "../../" 10 | 11 | SPEC CHECKSUMS: 12 | Starscream: 96cd79a6b7ef6a2ff2d00638c73bd195a5322586 13 | 14 | PODFILE CHECKSUM: 96d91933fe13671aaa81af8a8675ff7698068845 15 | 16 | COCOAPODS: 1.6.0.beta.1 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general-question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General Question 3 | about: 'Ask any question about the framework. ' 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Question 11 | > A description of what you want to know. 12 | 13 | ### Environment: 14 | - OS/Version: [e.g. iOS/13.3] 15 | - Starscream Version [e.g. 4.0.4] 16 | - Xcode version [e.g. 11.5] 17 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Starscream (3.0.6) 3 | 4 | DEPENDENCIES: 5 | - Starscream (from `../../`) 6 | 7 | EXTERNAL SOURCES: 8 | Starscream: 9 | :path: "../../" 10 | 11 | SPEC CHECKSUMS: 12 | Starscream: 96cd79a6b7ef6a2ff2d00638c73bd195a5322586 13 | 14 | PODFILE CHECKSUM: 96d91933fe13671aaa81af8a8675ff7698068845 15 | 16 | COCOAPODS: 1.6.0.beta.1 17 | -------------------------------------------------------------------------------- /examples/SimpleTest/README.md: -------------------------------------------------------------------------------- 1 | # Simple Test 2 | 3 | This is a very simple example on how to use Starscream. 4 | 5 | # Usage 6 | 7 | First make sure you have the gem dependencies of websocket server. 8 | 9 | ``` 10 | gem install em-websocket 11 | gem install faker 12 | ``` 13 | 14 | Next simply run: 15 | 16 | ``` 17 | ruby ws-server.rb 18 | ``` 19 | 20 | After that, start and run the xCode project. Echo to your heart's desire. -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Target Support Files/Starscream/Starscream-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double StarscreamVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char StarscreamVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Sources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTracking 6 | 7 | NSPrivacyTrackingDomains 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | NSPrivacyAccessedAPITypes 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_WebSocketsOrgEchoVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_WebSocketsOrgEchoVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/WebSocketsOrgEcho/URL+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Extensions.swift 3 | // Example 4 | // 5 | // Created by Kristaps Grinbergs on 08/10/2018. 6 | // Copyright © 2018 Kristaps Grinbergs. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension URL { 12 | init(staticString string: StaticString) { 13 | guard let url = URL(string: "\(string)") else { 14 | preconditionFailure("Invalid static URL string: \(string)") 15 | } 16 | 17 | self = url 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Target Support Files/Starscream/Starscream.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Starscream 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 4 | PODS_BUILD_DIR = ${BUILD_DIR} 5 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 6 | PODS_ROOT = ${SRCROOT} 7 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/../../.. 8 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 9 | SKIP_INSTALL = YES 10 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/WebSocketsOrgEcho/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // WebSocketsOrgEcho 4 | // 5 | // Created by Kristaps Grinbergs on 08/10/2018. 6 | // Copyright © 2018 Starscream. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | return true 18 | } 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report about a bug 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Describe the bug 11 | > A clear and concise description of what the bug is. 12 | 13 | ### Steps to Reproduce 14 | > Detailed steps to reproduce the problematic behavior: 15 | 16 | ### Expected behavior 17 | > A clear and concise description of what you expected to happen. 18 | 19 | ### Environment: 20 | - OS/Version: [e.g. iOS/13.3] 21 | - Starscream Version [e.g. 4.0.4] 22 | - Xcode version [e.g. 11.5] 23 | 24 | ### Additional context 25 | > Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /examples/AutobahnTest/.gitignore: -------------------------------------------------------------------------------- 1 | # CocoaPods 2 | # 3 | # We recommend against adding the Pods directory to your .gitignore. However 4 | # you should judge for yourself, the pros and cons are mentioned at: 5 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control? 6 | # 7 | # Pods/ 8 | 9 | # Xcode 10 | .DS_Store 11 | build 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | profile 22 | *.moved-aside 23 | DerivedData 24 | .idea/ 25 | *.hmap 26 | *.xccheckout 27 | *.xcodeproj/*.xcworkspace -------------------------------------------------------------------------------- /examples/SimpleTest/.gitignore: -------------------------------------------------------------------------------- 1 | # CocoaPods 2 | # 3 | # We recommend against adding the Pods directory to your .gitignore. However 4 | # you should judge for yourself, the pros and cons are mentioned at: 5 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control? 6 | # 7 | # Pods/ 8 | 9 | # Xcode 10 | .DS_Store 11 | build 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | profile 22 | *.moved-aside 23 | DerivedData 24 | .idea/ 25 | *.hmap 26 | *.xccheckout 27 | *.xcodeproj/*.xcworkspace -------------------------------------------------------------------------------- /examples/SimpleTest/ws-server.rb: -------------------------------------------------------------------------------- 1 | require 'em-websocket' 2 | require 'faker' 3 | 4 | EM.run { 5 | EM::WebSocket.run(:host => "0.0.0.0", :port => 8080) do |ws| 6 | ws.onopen { |handshake| 7 | puts "WebSocket connection open" 8 | puts "origin: #{handshake.origin}" 9 | puts "headers: #{handshake.headers}" 10 | 11 | ws.send "Hello Client, you connected to #{handshake.path}" 12 | } 13 | 14 | ws.onerror do |error| 15 | puts "[error] #{error}" 16 | end 17 | 18 | ws.onclose { puts "Connection closed" } 19 | 20 | ws.onmessage { |msg| 21 | puts "message from client: #{msg}" 22 | ws.send +Faker::Hacker.say_something_smart 23 | } 24 | end 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature_request 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### What do you want to happen? 11 | > Please replace this with the general overview of the feature that you'd like to have. 12 | 13 | ### What happens now? 14 | > Please replace this with of what is happening currently. 15 | 16 | ### Demo Code 17 | > Any demo code that may used to implement/use the desired feature. 18 | 19 | ### Describe alternatives you've considered 20 | > A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | ### Additional context 23 | > Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /examples/SimpleTest/SimpleTest.xcodeproj/xcuserdata/dalton.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SimpleTest.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | 5C765ADA199A6DAA003D9110 16 | 17 | primary 18 | 19 | 20 | 5C765AEC199A6DAA003D9110 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Local Podspecs/Starscream.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Starscream", 3 | "version": "3.0.6", 4 | "summary": "A conforming WebSocket RFC 6455 client library in Swift.", 5 | "homepage": "https://github.com/daltoniam/Starscream", 6 | "license": "Apache License, Version 2.0", 7 | "authors": { 8 | "Dalton Cherry": "http://daltoniam.com", 9 | "Austin Cherry": "http://austincherry.me" 10 | }, 11 | "source": { 12 | "git": "https://github.com/daltoniam/Starscream.git", 13 | "tag": "3.0.6" 14 | }, 15 | "social_media_url": "http://twitter.com/daltoniam", 16 | "platforms": { 17 | "ios": "8.0", 18 | "osx": "10.10", 19 | "tvos": "9.0", 20 | "watchos": "2.0" 21 | }, 22 | "source_files": "Sources/**/*.swift", 23 | "swift_version": "4.2" 24 | } 25 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Starscream" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Starscream/Starscream.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "Starscream" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*.*.*" 7 | jobs: 8 | release: 9 | runs-on: macos-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Set Latest Tag 14 | id: vars 15 | run: echo "tag=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_OUTPUT 16 | - uses: ruby/setup-ruby@v1 17 | with: 18 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 19 | - run: bundle exec fastlane test 20 | - run: bundle exec fastlane release 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} 23 | COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }} 24 | TAG: ${{ steps.vars.outputs.tag }} 25 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Starscream" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 5 | OTHER_CFLAGS = $(inherited) -iquote "${PODS_CONFIGURATION_BUILD_DIR}/Starscream/Starscream.framework/Headers" 6 | OTHER_LDFLAGS = $(inherited) -framework "Starscream" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /fastlane/Fastfile: -------------------------------------------------------------------------------- 1 | default_platform(:ios) 2 | 3 | update_fastlane 4 | 5 | 6 | desc "Run tests" 7 | lane :test do 8 | run_tests( 9 | devices: ["iPhone 14 Pro", "iPad Pro (11-inch) (4th generation)"], 10 | ) 11 | end 12 | 13 | platform :ios do 14 | desc "Deploy new version" 15 | lane :release do 16 | version = version_bump_podspec(path: "Starscream.podspec", version_number: ENV["TAG"]) 17 | changelog = changelog_from_git_commits(merge_commit_filtering: "exclude_merges") 18 | 19 | github_release = set_github_release( 20 | repository_name: "daltoniam/starscream", 21 | api_token: ENV["GITHUB_TOKEN"], 22 | name: version, 23 | tag_name: version, 24 | description: changelog, 25 | commitish: "master" 26 | ) 27 | pod_push(allow_warnings: false, verbose: true) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Starscream.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "Starscream" 3 | s.version = "4.0.4" 4 | s.summary = "A conforming WebSocket RFC 6455 client library in Swift." 5 | s.homepage = "https://github.com/daltoniam/Starscream" 6 | s.license = 'Apache License, Version 2.0' 7 | s.author = {'Dalton Cherry' => 'http://daltoniam.com', 'Austin Cherry' => 'http://austincherry.me'} 8 | s.source = { :git => 'https://github.com/daltoniam/Starscream.git', :tag => "#{s.version}"} 9 | s.social_media_url = 'http://twitter.com/daltoniam' 10 | s.ios.deployment_target = '12.0' 11 | s.osx.deployment_target = '10.13' 12 | s.tvos.deployment_target = '12.0' 13 | s.watchos.deployment_target = '2.0' 14 | s.source_files = 'Sources/**/*.swift' 15 | s.swift_version = '5.0' 16 | s.resource_bundles = { 17 | 'Starscream_Privacy' => ['Sources/PrivacyInfo.xcprivacy'], 18 | } 19 | end 20 | -------------------------------------------------------------------------------- /Sources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /fastlane/README.md: -------------------------------------------------------------------------------- 1 | fastlane documentation 2 | ---- 3 | 4 | # Installation 5 | 6 | Make sure you have the latest version of the Xcode command line tools installed: 7 | 8 | ```sh 9 | xcode-select --install 10 | ``` 11 | 12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) 13 | 14 | # Available Actions 15 | 16 | ### test 17 | 18 | ```sh 19 | [bundle exec] fastlane test 20 | ``` 21 | 22 | Run tests 23 | 24 | ---- 25 | 26 | 27 | ## iOS 28 | 29 | ### ios release 30 | 31 | ```sh 32 | [bundle exec] fastlane ios release 33 | ``` 34 | 35 | Deploy new version 36 | 37 | ---- 38 | 39 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. 40 | 41 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). 42 | 43 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). 44 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Target Support Files/Starscream/Starscream-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 3.0.6 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/Compression/Compression.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Compression.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 2/4/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | 25 | public protocol CompressionHandler { 26 | func load(headers: [String: String]) 27 | func decompress(data: Data, isFinal: Bool) -> Data? 28 | func compress(data: Data) -> Data? 29 | } 30 | -------------------------------------------------------------------------------- /examples/SimpleTest/SimpleTest/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "40x40", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "60x60", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "60x60", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "ipad", 25 | "size" : "29x29", 26 | "scale" : "1x" 27 | }, 28 | { 29 | "idiom" : "ipad", 30 | "size" : "29x29", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "40x40", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "40x40", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "76x76", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "76x76", 51 | "scale" : "2x" 52 | } 53 | ], 54 | "info" : { 55 | "version" : 1, 56 | "author" : "xcode" 57 | } 58 | } -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/WebSocketsOrgEcho/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // WebSocketsOrgEcho 4 | // 5 | // Created by Kristaps Grinbergs on 08/10/2018. 6 | // Copyright © 2018 Starscream. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | import Starscream 12 | 13 | class ViewController: UIViewController, WebSocketDelegate { 14 | 15 | var socket: WebSocket = WebSocket(url: URL(staticString: "wss://echo.websocket.org")) 16 | 17 | func websocketDidConnect(socket: WebSocketClient) { 18 | print("websocketDidConnect") 19 | } 20 | 21 | func websocketDidDisconnect(socket: WebSocketClient, error: Error?) { 22 | print("websocketDidDisconnect", error ?? "") 23 | } 24 | 25 | func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { 26 | print("websocketDidReceiveMessage", text) 27 | } 28 | 29 | func websocketDidReceiveData(socket: WebSocketClient, data: Data) { 30 | print("websocketDidReceiveData", data) 31 | } 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | socket.delegate = self 37 | } 38 | 39 | @IBAction func connect(_ sender: Any) { 40 | socket.connect() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/SimpleTest/SimpleTest/Images.xcassets/LaunchImage.launchimage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "orientation" : "portrait", 5 | "idiom" : "iphone", 6 | "extent" : "full-screen", 7 | "minimum-system-version" : "7.0", 8 | "scale" : "2x" 9 | }, 10 | { 11 | "orientation" : "portrait", 12 | "idiom" : "iphone", 13 | "subtype" : "retina4", 14 | "extent" : "full-screen", 15 | "minimum-system-version" : "7.0", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "orientation" : "portrait", 20 | "idiom" : "ipad", 21 | "extent" : "full-screen", 22 | "minimum-system-version" : "7.0", 23 | "scale" : "1x" 24 | }, 25 | { 26 | "orientation" : "landscape", 27 | "idiom" : "ipad", 28 | "extent" : "full-screen", 29 | "minimum-system-version" : "7.0", 30 | "scale" : "1x" 31 | }, 32 | { 33 | "orientation" : "portrait", 34 | "idiom" : "ipad", 35 | "extent" : "full-screen", 36 | "minimum-system-version" : "7.0", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "orientation" : "landscape", 41 | "idiom" : "ipad", 42 | "extent" : "full-screen", 43 | "minimum-system-version" : "7.0", 44 | "scale" : "2x" 45 | } 46 | ], 47 | "info" : { 48 | "version" : 1, 49 | "author" : "xcode" 50 | } 51 | } -------------------------------------------------------------------------------- /Sources/Starscream.h: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Starscream.h 4 | // Starscream 5 | // 6 | // Created by Austin Cherry on 9/25/14. 7 | // Copyright © 2014 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | #import 24 | 25 | //! Project version number for Starscream. 26 | FOUNDATION_EXPORT double StarscreamVersionNumber; 27 | 28 | //! Project version string for Starscream. 29 | FOUNDATION_EXPORT const unsigned char StarscreamVersionString[]; 30 | 31 | // In this header, you should import all the public headers of your framework using statements like #import 32 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | // 4 | // Package.Swift 5 | // Starscream 6 | // 7 | // Created by Dalton Cherry on 5/16/15. 8 | // Copyright (c) 2014-2016 Dalton Cherry. 9 | // 10 | // Licensed under the Apache License, Version 2.0 (the "License"); 11 | // you may not use this file except in compliance with the License. 12 | // You may obtain a copy of the License at 13 | // 14 | // http://www.apache.org/licenses/LICENSE-2.0 15 | // 16 | // Unless required by applicable law or agreed to in writing, software 17 | // distributed under the License is distributed on an "AS IS" BASIS, 18 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 19 | // See the License for the specific language governing permissions and 20 | // limitations under the License. 21 | // 22 | 23 | import PackageDescription 24 | 25 | let package = Package( 26 | name: "Starscream", 27 | products: [ 28 | .library(name: "Starscream", targets: ["Starscream"]) 29 | ], 30 | dependencies: [], 31 | targets: [ 32 | .target(name: "Starscream", 33 | path: "Sources", 34 | resources: [.copy("PrivacyInfo.xcprivacy")]) 35 | ] 36 | ) 37 | 38 | #if os(Linux) 39 | package.dependencies.append(.package(url: "https://github.com/apple/swift-nio-zlib-support.git", from: "1.0.0")) 40 | #endif 41 | -------------------------------------------------------------------------------- /Sources/Engine/Engine.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Engine.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 6/15/19 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | 25 | public protocol EngineDelegate: AnyObject { 26 | func didReceive(event: WebSocketEvent) 27 | } 28 | 29 | public protocol Engine { 30 | func register(delegate: EngineDelegate) 31 | func start(request: URLRequest) 32 | func stop(closeCode: UInt16) 33 | func forceStop() 34 | func write(data: Data, opcode: FrameOpCode, completion: (() -> ())?) 35 | func write(string: String, completion: (() -> ())?) 36 | } 37 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/WebSocketsOrgEcho/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/AutobahnTest/Autobahn/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /examples/SimpleTest/SimpleTest.xcodeproj/project.xcworkspace/xcshareddata/SimpleTest.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | 4D925893-CAFA-423E-A2A1-B4DD743323D3 9 | IDESourceControlProjectName 10 | SimpleTest 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | C86D95FCAEB1FEA0694B5D4AC7241D7E5F42F31D 14 | https://github.com/daltoniam/Starscream 15 | 16 | IDESourceControlProjectPath 17 | examples/SimpleTest/SimpleTest.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | C86D95FCAEB1FEA0694B5D4AC7241D7E5F42F31D 21 | ../../../.. 22 | 23 | IDESourceControlProjectURL 24 | https://github.com/daltoniam/Starscream 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | C86D95FCAEB1FEA0694B5D4AC7241D7E5F42F31D 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | C86D95FCAEB1FEA0694B5D4AC7241D7E5F42F31D 36 | IDESourceControlWCCName 37 | Starscream 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/SimpleTest/SimpleTest/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | UIMainStoryboardFile 31 | Main 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /examples/AutobahnTest/Autobahn/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/Security/Security.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Security.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 3/16/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | 25 | public enum SecurityErrorCode: UInt16 { 26 | case acceptFailed = 1 27 | case pinningFailed = 2 28 | } 29 | 30 | public enum PinningState { 31 | case success 32 | case failed(CFError?) 33 | } 34 | 35 | // CertificatePinning protocol provides an interface for Transports to handle Certificate 36 | // or Public Key Pinning. 37 | public protocol CertificatePinning: AnyObject { 38 | func evaluateTrust(trust: SecTrust, domain: String?, completion: ((PinningState) -> ())) 39 | } 40 | 41 | // validates the "Sec-WebSocket-Accept" header as defined 1.3 of the RFC 6455 42 | // https://tools.ietf.org/html/rfc6455#section-1.3 43 | public protocol HeaderValidator: AnyObject { 44 | func validate(headers: [String: String], key: String) -> Error? 45 | } 46 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/WebSocketsOrgEcho/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Tests/StarscreamTests/StarscreamTests.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // StarscreamTests.swift 4 | // StarscreamTests 5 | // 6 | // Created by Austin Cherry on 9/25/14. 7 | // Copyright © 2014 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import XCTest 24 | 25 | class StarscreamTests: XCTestCase { 26 | 27 | override func setUp() { 28 | super.setUp() 29 | // Put setup code here. This method is called before the invocation of each test method in the class. 30 | } 31 | 32 | override func tearDown() { 33 | // Put teardown code here. This method is called after the invocation of each test method in the class. 34 | super.tearDown() 35 | } 36 | 37 | func testExample() { 38 | // This is an example of a functional test case. 39 | XCTAssert(true, "Pass") 40 | } 41 | 42 | func testPerformanceExample() { 43 | // This is an example of a performance test case. 44 | self.measure() { 45 | // Put the code you want to measure the time of here. 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Server/Server.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Server.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 4/2/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | 25 | public enum ConnectionEvent { 26 | case connected([String: String]) 27 | case disconnected(String, UInt16) 28 | case text(String) 29 | case binary(Data) 30 | case pong(Data?) 31 | case ping(Data?) 32 | case error(Error) 33 | } 34 | 35 | public protocol Connection { 36 | func write(data: Data, opcode: FrameOpCode) 37 | } 38 | 39 | public protocol ConnectionDelegate: AnyObject { 40 | func didReceive(event: ServerEvent) 41 | } 42 | 43 | public enum ServerEvent { 44 | case connected(Connection, [String: String]) 45 | case disconnected(Connection, String, UInt16) 46 | case text(Connection, String) 47 | case binary(Connection, Data) 48 | case pong(Connection, Data?) 49 | case ping(Connection, Data?) 50 | } 51 | 52 | public protocol Server { 53 | func start(address: String, port: UInt16) -> Error? 54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /examples/AutobahnTest/Autobahn/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/WebSocketsOrgEcho/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /Sources/DataBytes/Data+Extensions.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Data+Extensions.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 3/27/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Fix for deprecation warnings 10 | // 11 | // Licensed under the Apache License, Version 2.0 (the "License"); 12 | // you may not use this file except in compliance with the License. 13 | // You may obtain a copy of the License at 14 | // 15 | // http://www.apache.org/licenses/LICENSE-2.0 16 | // 17 | // Unless required by applicable law or agreed to in writing, software 18 | // distributed under the License is distributed on an "AS IS" BASIS, 19 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | // See the License for the specific language governing permissions and 21 | // limitations under the License. 22 | // 23 | ////////////////////////////////////////////////////////////////////////////////////////////////// 24 | 25 | import Foundation 26 | 27 | internal extension Data { 28 | struct ByteError: Swift.Error {} 29 | 30 | #if swift(>=5.0) 31 | func withUnsafeBytes(_ completion: (UnsafePointer) throws -> ResultType) rethrows -> ResultType { 32 | return try withUnsafeBytes { 33 | if let baseAddress = $0.baseAddress, $0.count > 0 { 34 | return try completion(baseAddress.assumingMemoryBound(to: ContentType.self)) 35 | } else { 36 | throw ByteError() 37 | } 38 | } 39 | } 40 | #endif 41 | 42 | #if swift(>=5.0) 43 | mutating func withUnsafeMutableBytes(_ completion: (UnsafeMutablePointer) throws -> ResultType) rethrows -> ResultType { 44 | return try withUnsafeMutableBytes { 45 | if let baseAddress = $0.baseAddress, $0.count > 0 { 46 | return try completion(baseAddress.assumingMemoryBound(to: ContentType.self)) 47 | } else { 48 | throw ByteError() 49 | } 50 | } 51 | } 52 | #endif 53 | } 54 | -------------------------------------------------------------------------------- /examples/AutobahnTest/Autobahn/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Autobahn 4 | // 5 | // Created by Dalton Cherry on 7/24/15. 6 | // Copyright (c) 2015 vluxe. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Sources/Transport/Transport.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Transport.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 1/23/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | 25 | public enum ConnectionState { 26 | /// Ready connections can send and receive data 27 | case connected 28 | 29 | /// Waiting connections have not yet been started, or do not have a viable network 30 | case waiting 31 | 32 | /// Cancelled connections have been invalidated by the client and will send no more events 33 | case cancelled 34 | 35 | /// Failed connections are disconnected and can no longer send or receive data 36 | case failed(Error?) 37 | 38 | /// Viability (connection status) of the connection has updated 39 | /// e.g. connection is down, connection came back up, etc. 40 | case viability(Bool) 41 | 42 | /// Connection ca be upgraded to wifi from cellular. 43 | /// You should consider reconnecting to take advantage of this. 44 | case shouldReconnect(Bool) 45 | 46 | /// Received data 47 | case receive(Data) 48 | 49 | /// Remote peer has closed the network connection. 50 | case peerClosed 51 | } 52 | 53 | public protocol TransportEventClient: AnyObject { 54 | func connectionChanged(state: ConnectionState) 55 | } 56 | 57 | public protocol Transport: AnyObject { 58 | func register(delegate: TransportEventClient) 59 | func connect(url: URL, timeout: Double, certificatePinning: CertificatePinning?) 60 | func disconnect() 61 | func write(data: Data, completion: @escaping ((Error?) -> ())) 62 | var usingTLS: Bool { get } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/MockTransport.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // MockTransport.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 1/29/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | @testable import Starscream 25 | 26 | public class MockTransport: Transport { 27 | 28 | public var usingTLS: Bool { 29 | return false 30 | } 31 | private weak var delegate: TransportEventClient? 32 | 33 | private let id: String 34 | weak var server: MockServer? 35 | var uuid: String { 36 | return id 37 | } 38 | 39 | public init(server: MockServer) { 40 | self.server = server 41 | self.id = UUID().uuidString 42 | } 43 | 44 | public func register(delegate: TransportEventClient) { 45 | self.delegate = delegate 46 | } 47 | 48 | public func connect(url: URL, timeout: Double, certificatePinning: CertificatePinning?) { 49 | server?.connect(transport: self) 50 | delegate?.connectionChanged(state: .connected) 51 | } 52 | 53 | public func disconnect() { 54 | server?.disconnect(uuid: uuid) 55 | } 56 | 57 | public func write(data: Data, completion: @escaping ((Error?) -> ())) { 58 | server?.write(data: data, uuid: uuid) 59 | } 60 | 61 | public func received(data: Data) { 62 | delegate?.connectionChanged(state: .receive(data)) 63 | } 64 | 65 | public func getSecurityData() -> (SecTrust?, String?) { 66 | return (nil, nil) 67 | } 68 | } 69 | 70 | public class MockSecurity: CertificatePinning, HeaderValidator { 71 | 72 | public func evaluateTrust(trust: SecTrust, domain: String?, completion: ((PinningState) -> ())) { 73 | completion(.success) 74 | } 75 | 76 | public func validate(headers: [String: String], key: String) -> Error? { 77 | return nil 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/CompressionTests.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // CompressionTests.swift 4 | // 5 | // Created by Joseph Ross on 7/16/14. 6 | // Copyright © 2017 Joseph Ross. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | ////////////////////////////////////////////////////////////////////////////////////////////////// 21 | 22 | import XCTest 23 | @testable import Starscream 24 | 25 | class CompressionTests: XCTestCase { 26 | 27 | override func setUp() { 28 | super.setUp() 29 | // Put setup code here. This method is called before the invocation of each test method in the class. 30 | } 31 | 32 | override func tearDown() { 33 | // Put teardown code here. This method is called after the invocation of each test method in the class. 34 | super.tearDown() 35 | } 36 | 37 | func testBasic() { 38 | let compressor = Compressor(windowBits: 15)! 39 | let decompressor = Decompressor(windowBits: 15)! 40 | 41 | let rawData = "Hello, World! Hello, World! Hello, World! Hello, World! Hello, World!".data(using: .utf8)! 42 | 43 | let compressed = try! compressor.compress(rawData) 44 | let uncompressed = try! decompressor.decompress(compressed, finish: true) 45 | 46 | XCTAssert(rawData == uncompressed) 47 | } 48 | 49 | func testHugeData() { 50 | let compressor = Compressor(windowBits: 15)! 51 | let decompressor = Decompressor(windowBits: 15)! 52 | 53 | // 2 Gigs! 54 | // var rawData = Data(repeating: 0, count: 0x80000000) 55 | var rawData = Data(repeating: 0, count: 0x80000) 56 | let rawDataLen = rawData.count 57 | rawData.withUnsafeMutableBytes { (ptr: UnsafeMutablePointer) -> Void in 58 | arc4random_buf(ptr, rawDataLen) 59 | } 60 | 61 | let compressed = try! compressor.compress(rawData) 62 | let uncompressed = try! decompressor.decompress(compressed, finish: true) 63 | 64 | XCTAssert(rawData == uncompressed) 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/WebSocketsOrgEcho/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # CocoaPods 2 | # 3 | # We recommend against adding the Pods directory to your .gitignore. However 4 | # you should judge for yourself, the pros and cons are mentioned at: 5 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control? 6 | # 7 | Pods/ 8 | 9 | # Xcode 10 | .DS_Store 11 | build 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | profile 22 | *.moved-aside 23 | DerivedData 24 | .idea/ 25 | *.hmap 26 | *.xccheckout 27 | *.xcodeproj/*.xcworkspace 28 | 29 | 30 | # Created by https://www.gitignore.io/api/swift,swiftpm 31 | 32 | ### Swift ### 33 | # Xcode 34 | # 35 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 36 | 37 | ## Build generated 38 | build/ 39 | DerivedData/ 40 | 41 | ## Various settings 42 | *.pbxuser 43 | !default.pbxuser 44 | *.mode1v3 45 | !default.mode1v3 46 | *.mode2v3 47 | !default.mode2v3 48 | *.perspectivev3 49 | !default.perspectivev3 50 | xcuserdata/ 51 | 52 | ## Other 53 | *.moved-aside 54 | *.xccheckout 55 | *.xcscmblueprint 56 | 57 | ## Obj-C/Swift specific 58 | *.hmap 59 | *.ipa 60 | *.dSYM.zip 61 | *.dSYM 62 | 63 | ## Playgrounds 64 | timeline.xctimeline 65 | playground.xcworkspace 66 | 67 | # Swift Package Manager 68 | # 69 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 70 | # Packages/ 71 | # Package.pins 72 | # Package.resolved 73 | .build/ 74 | 75 | # CocoaPods 76 | # 77 | # We recommend against adding the Pods directory to your .gitignore. However 78 | # you should judge for yourself, the pros and cons are mentioned at: 79 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 80 | # 81 | # Pods/ 82 | # 83 | # Add this line if you want to avoid checking in source code from the Xcode workspace 84 | # *.xcworkspace 85 | 86 | # Carthage 87 | # 88 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 89 | # Carthage/Checkouts 90 | 91 | Carthage/Build 92 | 93 | # fastlane 94 | # 95 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 96 | # screenshots whenever they are needed. 97 | # For more information about the recommended setup visit: 98 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 99 | 100 | fastlane/report.xml 101 | fastlane/Preview.html 102 | fastlane/screenshots/**/*.png 103 | fastlane/test_output 104 | 105 | # Code Injection 106 | # 107 | # After new code Injection tools there's a generated folder /iOSInjectionProject 108 | # https://github.com/johnno1962/injectionforxcode 109 | 110 | iOSInjectionProject/ 111 | 112 | ### SwiftPM ### 113 | Packages 114 | xcuserdata 115 | *.xcodeproj 116 | 117 | 118 | # End of https://www.gitignore.io/api/swift,swiftpm 119 | -------------------------------------------------------------------------------- /examples/SimpleTest/SimpleTest/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // AppDelegate.swift 4 | // SimpleTest 5 | // 6 | // Created by Dalton Cherry on 8/12/14. 7 | // Copyright © 2014 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import UIKit 24 | 25 | @UIApplicationMain 26 | class AppDelegate: UIResponder, UIApplicationDelegate { 27 | 28 | var window: UIWindow? 29 | 30 | 31 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 32 | // Override point for customization after application launch. 33 | return true 34 | } 35 | 36 | func applicationWillResignActive(_ application: UIApplication) { 37 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 38 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 39 | } 40 | 41 | func applicationDidEnterBackground(_ application: UIApplication) { 42 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 43 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 44 | } 45 | 46 | func applicationWillEnterForeground(_ application: UIApplication) { 47 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 48 | } 49 | 50 | func applicationDidBecomeActive(_ application: UIApplication) { 51 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 52 | } 53 | 54 | func applicationWillTerminate(_ application: UIApplication) { 55 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 56 | } 57 | 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /examples/SimpleTest/SimpleTest/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /examples/SimpleTest/SimpleTest/ViewController.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // ViewController.swift 4 | // SimpleTest 5 | // 6 | // Created by Dalton Cherry on 8/12/14. 7 | // Copyright © 2014 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import UIKit 24 | import Starscream 25 | 26 | class ViewController: UIViewController, WebSocketDelegate { 27 | var socket: WebSocket! 28 | var isConnected = false 29 | let server = WebSocketServer() 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | //https://echo.websocket.org 34 | var request = URLRequest(url: URL(string: "http://localhost:8080")!) //https://localhost:8080 35 | request.timeoutInterval = 5 36 | socket = WebSocket(request: request) 37 | socket.delegate = self 38 | socket.connect() 39 | } 40 | 41 | // MARK: - WebSocketDelegate 42 | func didReceive(event: Starscream.WebSocketEvent, client: Starscream.WebSocketClient) { 43 | switch event { 44 | case .connected(let headers): 45 | isConnected = true 46 | print("websocket is connected: \(headers)") 47 | case .disconnected(let reason, let code): 48 | isConnected = false 49 | print("websocket is disconnected: \(reason) with code: \(code)") 50 | case .text(let string): 51 | print("Received text: \(string)") 52 | case .binary(let data): 53 | print("Received data: \(data.count)") 54 | case .ping(_): 55 | break 56 | case .pong(_): 57 | break 58 | case .viabilityChanged(_): 59 | break 60 | case .reconnectSuggested(_): 61 | break 62 | case .cancelled: 63 | isConnected = false 64 | case .error(let error): 65 | isConnected = false 66 | handleError(error) 67 | case .peerClosed: 68 | break 69 | } 70 | } 71 | 72 | func handleError(_ error: Error?) { 73 | if let e = error as? WSError { 74 | print("websocket encountered an error: \(e.message)") 75 | } else if let e = error { 76 | print("websocket encountered an error: \(e.localizedDescription)") 77 | } else { 78 | print("websocket encountered an error") 79 | } 80 | } 81 | 82 | // MARK: Write Text Action 83 | 84 | @IBAction func writeText(_ sender: UIBarButtonItem) { 85 | socket.write(string: "hello there!") 86 | } 87 | 88 | // MARK: Disconnect Action 89 | 90 | @IBAction func disconnect(_ sender: UIBarButtonItem) { 91 | if isConnected { 92 | sender.title = "Connect" 93 | socket.disconnect() 94 | } else { 95 | sender.title = "Disconnect" 96 | socket.connect() 97 | } 98 | } 99 | 100 | } 101 | 102 | -------------------------------------------------------------------------------- /examples/AutobahnTest/Autobahn/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Sources/Security/FoundationSecurity.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // FoundationSecurity.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 3/16/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | import CommonCrypto 25 | 26 | public enum FoundationSecurityError: Error { 27 | case invalidRequest 28 | } 29 | 30 | public class FoundationSecurity { 31 | var allowSelfSigned = false 32 | 33 | public init(allowSelfSigned: Bool = false) { 34 | self.allowSelfSigned = allowSelfSigned 35 | } 36 | 37 | 38 | } 39 | 40 | extension FoundationSecurity: CertificatePinning { 41 | public func evaluateTrust(trust: SecTrust, domain: String?, completion: ((PinningState) -> ())) { 42 | if allowSelfSigned { 43 | completion(.success) 44 | return 45 | } 46 | 47 | SecTrustSetPolicies(trust, SecPolicyCreateSSL(true, domain as NSString?)) 48 | 49 | handleSecurityTrust(trust: trust, completion: completion) 50 | } 51 | 52 | private func handleSecurityTrust(trust: SecTrust, completion: ((PinningState) -> ())) { 53 | if #available(iOS 12.0, OSX 10.14, watchOS 5.0, tvOS 12.0, *) { 54 | var error: CFError? 55 | if SecTrustEvaluateWithError(trust, &error) { 56 | completion(.success) 57 | } else { 58 | completion(.failed(error)) 59 | } 60 | } else { 61 | handleOldSecurityTrust(trust: trust, completion: completion) 62 | } 63 | } 64 | 65 | private func handleOldSecurityTrust(trust: SecTrust, completion: ((PinningState) -> ())) { 66 | var result: SecTrustResultType = .unspecified 67 | SecTrustEvaluate(trust, &result) 68 | if result == .unspecified || result == .proceed { 69 | completion(.success) 70 | } else { 71 | let e = CFErrorCreate(kCFAllocatorDefault, "FoundationSecurityError" as NSString?, Int(result.rawValue), nil) 72 | completion(.failed(e)) 73 | } 74 | } 75 | } 76 | 77 | extension FoundationSecurity: HeaderValidator { 78 | public func validate(headers: [String: String], key: String) -> Error? { 79 | if let acceptKey = headers[HTTPWSHeader.acceptName] { 80 | let sha = "\(key)258EAFA5-E914-47DA-95CA-C5AB0DC85B11".sha1Base64() 81 | if sha != acceptKey { 82 | return WSError(type: .securityError, message: "accept header doesn't match", code: SecurityErrorCode.acceptFailed.rawValue) 83 | } 84 | } 85 | return nil 86 | } 87 | } 88 | 89 | private extension String { 90 | func sha1Base64() -> String { 91 | let data = self.data(using: .utf8)! 92 | let pointer = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> [UInt8] in 93 | var digest = [UInt8](repeating: 0, count:Int(CC_SHA1_DIGEST_LENGTH)) 94 | CC_SHA1(bytes.baseAddress, CC_LONG(data.count), &digest) 95 | return digest 96 | } 97 | return Data(pointer).base64EncodedString() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Sources/Framer/FoundationHTTPServerHandler.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // FoundationHTTPHandler.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 4/2/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | 25 | public class FoundationHTTPServerHandler: HTTPServerHandler { 26 | var buffer = Data() 27 | weak var delegate: HTTPServerDelegate? 28 | let getVerb: NSString = "GET" 29 | 30 | public func register(delegate: HTTPServerDelegate) { 31 | self.delegate = delegate 32 | } 33 | 34 | public func createResponse(headers: [String: String]) -> Data { 35 | #if os(watchOS) 36 | //TODO: build response header 37 | return Data() 38 | #else 39 | let response = CFHTTPMessageCreateResponse(kCFAllocatorDefault, HTTPWSHeader.switchProtocolCode, 40 | nil, kCFHTTPVersion1_1).takeRetainedValue() 41 | 42 | //TODO: add other values to make a proper response here... 43 | //TODO: also sec key thing (Sec-WebSocket-Key) 44 | for (key, value) in headers { 45 | CFHTTPMessageSetHeaderFieldValue(response, key as CFString, value as CFString) 46 | } 47 | guard let cfData = CFHTTPMessageCopySerializedMessage(response)?.takeRetainedValue() else { 48 | return Data() 49 | } 50 | return cfData as Data 51 | #endif 52 | } 53 | 54 | public func parse(data: Data) { 55 | buffer.append(data) 56 | if parseContent(data: buffer) { 57 | buffer = Data() 58 | } 59 | } 60 | 61 | //returns true when the buffer should be cleared 62 | func parseContent(data: Data) -> Bool { 63 | var pointer = [UInt8]() 64 | data.withUnsafeBytes { pointer.append(contentsOf: $0) } 65 | #if os(watchOS) 66 | //TODO: parse data 67 | return false 68 | #else 69 | let response = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, true).takeRetainedValue() 70 | if !CFHTTPMessageAppendBytes(response, pointer, data.count) { 71 | return false //not enough data, wait for more 72 | } 73 | if !CFHTTPMessageIsHeaderComplete(response) { 74 | return false //not enough data, wait for more 75 | } 76 | if let method = CFHTTPMessageCopyRequestMethod(response)?.takeRetainedValue() { 77 | if (method as NSString) != getVerb { 78 | delegate?.didReceive(event: .failure(HTTPUpgradeError.invalidData)) 79 | return true 80 | } 81 | } 82 | 83 | if let cfHeaders = CFHTTPMessageCopyAllHeaderFields(response) { 84 | let nsHeaders = cfHeaders.takeRetainedValue() as NSDictionary 85 | var headers = [String: String]() 86 | for (key, value) in nsHeaders { 87 | if let key = key as? String, let value = value as? String { 88 | headers[key] = value 89 | } 90 | } 91 | delegate?.didReceive(event: .success(headers)) 92 | return true 93 | } 94 | 95 | delegate?.didReceive(event: .failure(HTTPUpgradeError.invalidData)) 96 | return true 97 | #endif 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Starscream.xcodeproj/xcshareddata/xcschemes/Starscream.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 38 | 39 | 40 | 41 | 43 | 49 | 50 | 53 | 54 | 55 | 56 | 57 | 67 | 68 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /Sources/Framer/FrameCollector.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // FrameCollector.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 1/24/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | 25 | public protocol FrameCollectorDelegate: AnyObject { 26 | func didForm(event: FrameCollector.Event) 27 | func decompress(data: Data, isFinal: Bool) -> Data? 28 | } 29 | 30 | public class FrameCollector { 31 | public enum Event { 32 | case text(String) 33 | case binary(Data) 34 | case pong(Data?) 35 | case ping(Data?) 36 | case error(Error) 37 | case closed(String, UInt16) 38 | } 39 | weak var delegate: FrameCollectorDelegate? 40 | var buffer = Data() 41 | var frameCount = 0 42 | var isText = false //was the first frame a text frame or a binary frame? 43 | var needsDecompression = false 44 | 45 | public func add(frame: Frame) { 46 | //check single frame action and out of order frames 47 | if frame.opcode == .connectionClose { 48 | var code = frame.closeCode 49 | var reason = "connection closed by server" 50 | if let customCloseReason = String(data: frame.payload, encoding: .utf8) { 51 | reason = customCloseReason 52 | } else { 53 | code = CloseCode.protocolError.rawValue 54 | } 55 | delegate?.didForm(event: .closed(reason, code)) 56 | return 57 | } else if frame.opcode == .pong { 58 | delegate?.didForm(event: .pong(frame.payload)) 59 | return 60 | } else if frame.opcode == .ping { 61 | delegate?.didForm(event: .ping(frame.payload)) 62 | return 63 | } else if frame.opcode == .continueFrame && frameCount == 0 { 64 | let errCode = CloseCode.protocolError.rawValue 65 | delegate?.didForm(event: .error(WSError(type: .protocolError, message: "first frame can't be a continue frame", code: errCode))) 66 | reset() 67 | return 68 | } else if frameCount > 0 && frame.opcode != .continueFrame { 69 | let errCode = CloseCode.protocolError.rawValue 70 | delegate?.didForm(event: .error(WSError(type: .protocolError, message: "second and beyond of fragment message must be a continue frame", code: errCode))) 71 | reset() 72 | return 73 | } 74 | if frameCount == 0 { 75 | isText = frame.opcode == .textFrame 76 | needsDecompression = frame.needsDecompression 77 | } 78 | 79 | let payload: Data 80 | if needsDecompression { 81 | payload = delegate?.decompress(data: frame.payload, isFinal: frame.isFin) ?? frame.payload 82 | } else { 83 | payload = frame.payload 84 | } 85 | buffer.append(payload) 86 | frameCount += 1 87 | 88 | if frame.isFin { 89 | if isText { 90 | if let string = String(data: buffer, encoding: .utf8) { 91 | delegate?.didForm(event: .text(string)) 92 | } else { 93 | let errCode = CloseCode.protocolError.rawValue 94 | delegate?.didForm(event: .error(WSError(type: .protocolError, message: "not valid UTF-8 data", code: errCode))) 95 | } 96 | } else { 97 | delegate?.didForm(event: .binary(buffer)) 98 | } 99 | reset() 100 | } 101 | } 102 | 103 | func reset() { 104 | buffer = Data() 105 | frameCount = 0 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Sources/Engine/NativeEngine.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // NativeEngine.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 6/15/19 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | 25 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 26 | public class NativeEngine: NSObject, Engine, URLSessionDataDelegate, URLSessionWebSocketDelegate { 27 | private var task: URLSessionWebSocketTask? 28 | weak var delegate: EngineDelegate? 29 | 30 | public func register(delegate: EngineDelegate) { 31 | self.delegate = delegate 32 | } 33 | 34 | public func start(request: URLRequest) { 35 | let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil) 36 | task = session.webSocketTask(with: request) 37 | doRead() 38 | task?.resume() 39 | } 40 | 41 | public func stop(closeCode: UInt16) { 42 | let closeCode = URLSessionWebSocketTask.CloseCode(rawValue: Int(closeCode)) ?? .normalClosure 43 | task?.cancel(with: closeCode, reason: nil) 44 | } 45 | 46 | public func forceStop() { 47 | stop(closeCode: UInt16(URLSessionWebSocketTask.CloseCode.abnormalClosure.rawValue)) 48 | } 49 | 50 | public func write(string: String, completion: (() -> ())?) { 51 | task?.send(.string(string), completionHandler: { (error) in 52 | completion?() 53 | }) 54 | } 55 | 56 | public func write(data: Data, opcode: FrameOpCode, completion: (() -> ())?) { 57 | switch opcode { 58 | case .binaryFrame: 59 | task?.send(.data(data), completionHandler: { (error) in 60 | completion?() 61 | }) 62 | case .textFrame: 63 | let text = String(data: data, encoding: .utf8)! 64 | write(string: text, completion: completion) 65 | case .ping: 66 | task?.sendPing(pongReceiveHandler: { (error) in 67 | completion?() 68 | }) 69 | default: 70 | break //unsupported 71 | } 72 | } 73 | 74 | private func doRead() { 75 | task?.receive { [weak self] (result) in 76 | switch result { 77 | case .success(let message): 78 | switch message { 79 | case .string(let string): 80 | self?.broadcast(event: .text(string)) 81 | case .data(let data): 82 | self?.broadcast(event: .binary(data)) 83 | @unknown default: 84 | break 85 | } 86 | break 87 | case .failure(let error): 88 | self?.broadcast(event: .error(error)) 89 | return 90 | } 91 | self?.doRead() 92 | } 93 | } 94 | 95 | private func broadcast(event: WebSocketEvent) { 96 | delegate?.didReceive(event: event) 97 | } 98 | 99 | public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { 100 | let p = `protocol` ?? "" 101 | broadcast(event: .connected([HTTPWSHeader.protocolName: p])) 102 | } 103 | 104 | public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { 105 | var r = "" 106 | if let d = reason { 107 | r = String(data: d, encoding: .utf8) ?? "" 108 | } 109 | broadcast(event: .disconnected(r, UInt16(closeCode.rawValue))) 110 | } 111 | 112 | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 113 | broadcast(event: .error(error)) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Sources/Framer/FoundationHTTPHandler.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // FoundationHTTPHandler.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 1/25/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | #if os(watchOS) 25 | public typealias FoundationHTTPHandler = StringHTTPHandler 26 | #else 27 | public class FoundationHTTPHandler: HTTPHandler { 28 | 29 | var buffer = Data() 30 | weak var delegate: HTTPHandlerDelegate? 31 | 32 | public init() { 33 | 34 | } 35 | 36 | public func convert(request: URLRequest) -> Data { 37 | let msg = CFHTTPMessageCreateRequest(kCFAllocatorDefault, request.httpMethod! as CFString, 38 | request.url! as CFURL, kCFHTTPVersion1_1).takeRetainedValue() 39 | if let headers = request.allHTTPHeaderFields { 40 | for (aKey, aValue) in headers { 41 | CFHTTPMessageSetHeaderFieldValue(msg, aKey as CFString, aValue as CFString) 42 | } 43 | } 44 | if let body = request.httpBody { 45 | CFHTTPMessageSetBody(msg, body as CFData) 46 | } 47 | guard let data = CFHTTPMessageCopySerializedMessage(msg) else { 48 | return Data() 49 | } 50 | return data.takeRetainedValue() as Data 51 | } 52 | 53 | public func parse(data: Data) -> Int { 54 | let offset = findEndOfHTTP(data: data) 55 | if offset > 0 { 56 | buffer.append(data.subdata(in: 0.. Bool { 68 | var pointer = [UInt8]() 69 | data.withUnsafeBytes { pointer.append(contentsOf: $0) } 70 | 71 | let response = CFHTTPMessageCreateEmpty(kCFAllocatorDefault, false).takeRetainedValue() 72 | if !CFHTTPMessageAppendBytes(response, pointer, data.count) { 73 | return false //not enough data, wait for more 74 | } 75 | if !CFHTTPMessageIsHeaderComplete(response) { 76 | return false //not enough data, wait for more 77 | } 78 | 79 | if let cfHeaders = CFHTTPMessageCopyAllHeaderFields(response) { 80 | let nsHeaders = cfHeaders.takeRetainedValue() as NSDictionary 81 | var headers = [String: String]() 82 | for (key, value) in nsHeaders { 83 | if let key = key as? String, let value = value as? String { 84 | headers[key] = value 85 | } 86 | } 87 | 88 | let code = CFHTTPMessageGetResponseStatusCode(response) 89 | if code != HTTPWSHeader.switchProtocolCode { 90 | delegate?.didReceiveHTTP(event: .failure(HTTPUpgradeError.notAnUpgrade(code, headers))) 91 | return true 92 | } 93 | 94 | delegate?.didReceiveHTTP(event: .success(headers)) 95 | return true 96 | } 97 | 98 | delegate?.didReceiveHTTP(event: .failure(HTTPUpgradeError.invalidData)) 99 | return true 100 | } 101 | 102 | public func register(delegate: HTTPHandlerDelegate) { 103 | self.delegate = delegate 104 | } 105 | 106 | private func findEndOfHTTP(data: Data) -> Int { 107 | let endBytes = [UInt8(ascii: "\r"), UInt8(ascii: "\n"), UInt8(ascii: "\r"), UInt8(ascii: "\n")] 108 | var pointer = [UInt8]() 109 | data.withUnsafeBytes { pointer.append(contentsOf: $0) } 110 | var k = 0 111 | for i in 0.. 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 80 | 82 | 88 | 89 | 90 | 91 | 95 | 96 | 97 | 98 | 99 | 100 | 106 | 108 | 114 | 115 | 116 | 117 | 119 | 120 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /Tests/MockServer.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // MockServer.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 1/29/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | @testable import Starscream 25 | 26 | public class MockConnection: Connection, HTTPServerDelegate, FramerEventClient, FrameCollectorDelegate { 27 | let transport: MockTransport 28 | private let httpHandler = FoundationHTTPServerHandler() 29 | private let framer = WSFramer(isServer: true) 30 | private let frameHandler = FrameCollector() 31 | private var didUpgrade = false 32 | public var onEvent: ((ConnectionEvent) -> Void)? 33 | fileprivate weak var delegate: ConnectionDelegate? 34 | 35 | init(transport: MockTransport) { 36 | self.transport = transport 37 | httpHandler.register(delegate: self) 38 | framer.register(delegate: self) 39 | frameHandler.delegate = self 40 | } 41 | 42 | func add(data: Data) { 43 | if !didUpgrade { 44 | httpHandler.parse(data: data) 45 | } else { 46 | framer.add(data: data) 47 | } 48 | } 49 | 50 | public func write(data: Data, opcode: FrameOpCode) { 51 | let wsData = framer.createWriteFrame(opcode: opcode, payload: data, isCompressed: false) 52 | transport.received(data: wsData) 53 | } 54 | 55 | /// MARK: - HTTPServerDelegate 56 | 57 | public func didReceive(event: HTTPEvent) { 58 | switch event { 59 | case .success(let headers): 60 | didUpgrade = true 61 | //TODO: add headers and key check? 62 | let response = httpHandler.createResponse(headers: [:]) 63 | transport.received(data: response) 64 | delegate?.didReceive(event: .connected(self, headers)) 65 | onEvent?(.connected(headers)) 66 | case .failure(let error): 67 | onEvent?(.error(error)) 68 | } 69 | } 70 | 71 | /// MARK: - FrameCollectorDelegate 72 | 73 | public func frameProcessed(event: FrameEvent) { 74 | switch event { 75 | case .frame(let frame): 76 | frameHandler.add(frame: frame) 77 | case .error(let error): 78 | onEvent?(.error(error)) 79 | } 80 | } 81 | 82 | public func didForm(event: FrameCollector.Event) { 83 | switch event { 84 | case .text(let string): 85 | delegate?.didReceive(event: .text(self, string)) 86 | onEvent?(.text(string)) 87 | case .binary(let data): 88 | delegate?.didReceive(event: .binary(self, data)) 89 | onEvent?(.binary(data)) 90 | case .pong(let data): 91 | delegate?.didReceive(event: .pong(self, data)) 92 | onEvent?(.pong(data)) 93 | case .ping(let data): 94 | delegate?.didReceive(event: .ping(self, data)) 95 | onEvent?(.ping(data)) 96 | case .closed(let reason, let code): 97 | delegate?.didReceive(event: .disconnected(self, reason, code)) 98 | onEvent?(.disconnected(reason, code)) 99 | case .error(let error): 100 | onEvent?(.error(error)) 101 | } 102 | } 103 | 104 | public func decompress(data: Data, isFinal: Bool) -> Data? { 105 | return nil 106 | } 107 | } 108 | 109 | 110 | public class MockServer: Server, ConnectionDelegate { 111 | fileprivate var connections = [String: MockConnection]() 112 | 113 | public var onEvent: ((ServerEvent) -> Void)? 114 | 115 | public func start(address: String, port: UInt16) -> Error? { 116 | return nil 117 | } 118 | 119 | public func connect(transport: MockTransport) { 120 | let conn = MockConnection(transport: transport) 121 | conn.delegate = self 122 | connections[transport.uuid] = conn 123 | } 124 | 125 | public func disconnect(uuid: String) { 126 | // guard let conn = connections[uuid] else { 127 | // return 128 | // } 129 | //TODO: force disconnect 130 | connections.removeValue(forKey: uuid) 131 | } 132 | 133 | public func write(data: Data, uuid: String) { 134 | guard let conn = connections[uuid] else { 135 | return 136 | } 137 | conn.add(data: data) 138 | } 139 | 140 | /// MARK: - MockConnectionDelegate 141 | public func didReceive(event: ServerEvent) { 142 | onEvent?(event) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Sources/Framer/StringHTTPHandler.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // StringHTTPHandler.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 8/25/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | 25 | public class StringHTTPHandler: HTTPHandler { 26 | 27 | var buffer = Data() 28 | weak var delegate: HTTPHandlerDelegate? 29 | 30 | public init() { 31 | 32 | } 33 | 34 | public func convert(request: URLRequest) -> Data { 35 | guard let url = request.url else { 36 | return Data() 37 | } 38 | 39 | var path = url.absoluteString 40 | let offset = (url.scheme?.count ?? 2) + 3 41 | path = String(path[path.index(path.startIndex, offsetBy: offset).. Int { 71 | let offset = findEndOfHTTP(data: data) 72 | if offset > 0 { 73 | buffer.append(data.subdata(in: 0.. Bool { 85 | guard let str = String(data: data, encoding: .utf8) else { 86 | delegate?.didReceiveHTTP(event: .failure(HTTPUpgradeError.invalidData)) 87 | return true 88 | } 89 | let splitArr = str.components(separatedBy: "\r\n") 90 | var code = -1 91 | var i = 0 92 | var headers = [String: String]() 93 | for str in splitArr { 94 | if i == 0 { 95 | let responseSplit = str.components(separatedBy: .whitespaces) 96 | guard responseSplit.count > 1 else { 97 | delegate?.didReceiveHTTP(event: .failure(HTTPUpgradeError.invalidData)) 98 | return true 99 | } 100 | if let c = Int(responseSplit[1]) { 101 | code = c 102 | } 103 | } else { 104 | guard let separatorIndex = str.firstIndex(of: ":") else { break } 105 | let key = str.prefix(upTo: separatorIndex).trimmingCharacters(in: .whitespaces) 106 | let val = str.suffix(from: str.index(after: separatorIndex)).trimmingCharacters(in: .whitespaces) 107 | headers[key.lowercased()] = val 108 | } 109 | i += 1 110 | } 111 | 112 | if code != HTTPWSHeader.switchProtocolCode { 113 | delegate?.didReceiveHTTP(event: .failure(HTTPUpgradeError.notAnUpgrade(code, headers))) 114 | return true 115 | } 116 | 117 | delegate?.didReceiveHTTP(event: .success(headers)) 118 | return true 119 | } 120 | 121 | public func register(delegate: HTTPHandlerDelegate) { 122 | self.delegate = delegate 123 | } 124 | 125 | private func findEndOfHTTP(data: Data) -> Int { 126 | let endBytes = [UInt8(ascii: "\r"), UInt8(ascii: "\n"), UInt8(ascii: "\r"), UInt8(ascii: "\n")] 127 | var pointer = [UInt8]() 128 | data.withUnsafeBytes { pointer.append(contentsOf: $0) } 129 | var k = 0 130 | for i in 0.. URLRequest { 52 | guard let url = request.url, let parts = url.getParts() else { 53 | return request 54 | } 55 | 56 | var req = request 57 | if request.value(forHTTPHeaderField: HTTPWSHeader.originName) == nil { 58 | var origin = url.absoluteString 59 | if let hostUrl = URL (string: "/", relativeTo: url) { 60 | origin = hostUrl.absoluteString 61 | origin.remove(at: origin.index(before: origin.endIndex)) 62 | } 63 | req.setValue(origin, forHTTPHeaderField: HTTPWSHeader.originName) 64 | } 65 | req.setValue(HTTPWSHeader.upgradeValue, forHTTPHeaderField: HTTPWSHeader.upgradeName) 66 | req.setValue(HTTPWSHeader.connectionValue, forHTTPHeaderField: HTTPWSHeader.connectionName) 67 | req.setValue(HTTPWSHeader.versionValue, forHTTPHeaderField: HTTPWSHeader.versionName) 68 | req.setValue(secKeyValue, forHTTPHeaderField: HTTPWSHeader.keyName) 69 | 70 | if req.allHTTPHeaderFields?["Cookie"] == nil { 71 | if let cookies = HTTPCookieStorage.shared.cookies(for: url), !cookies.isEmpty { 72 | let headers = HTTPCookie.requestHeaderFields(with: cookies) 73 | for (key, val) in headers { 74 | req.setValue(val, forHTTPHeaderField: key) 75 | } 76 | } 77 | } 78 | 79 | if supportsCompression { 80 | let val = "permessage-deflate; client_max_window_bits; server_max_window_bits=15" 81 | req.setValue(val, forHTTPHeaderField: HTTPWSHeader.extensionName) 82 | } 83 | let hostValue = req.allHTTPHeaderFields?[HTTPWSHeader.hostName] ?? "\(parts.host):\(parts.port)" 84 | req.setValue(hostValue, forHTTPHeaderField: HTTPWSHeader.hostName) 85 | return req 86 | } 87 | 88 | // generateWebSocketKey 16 random characters between a-z and return them as a base64 string 89 | public static func generateWebSocketKey() -> String { 90 | return Data((0..<16).map{ _ in UInt8.random(in: 97...122) }).base64EncodedString() 91 | } 92 | } 93 | 94 | public enum HTTPEvent { 95 | case success([String: String]) 96 | case failure(Error) 97 | } 98 | 99 | public protocol HTTPHandlerDelegate: AnyObject { 100 | func didReceiveHTTP(event: HTTPEvent) 101 | } 102 | 103 | public protocol HTTPHandler { 104 | func register(delegate: HTTPHandlerDelegate) 105 | func convert(request: URLRequest) -> Data 106 | func parse(data: Data) -> Int 107 | } 108 | 109 | public protocol HTTPServerDelegate: AnyObject { 110 | func didReceive(event: HTTPEvent) 111 | } 112 | 113 | public protocol HTTPServerHandler { 114 | func register(delegate: HTTPServerDelegate) 115 | func parse(data: Data) 116 | func createResponse(headers: [String: String]) -> Data 117 | } 118 | 119 | public struct URLParts { 120 | let port: Int 121 | let host: String 122 | let isTLS: Bool 123 | } 124 | 125 | public extension URL { 126 | /// isTLSScheme returns true if the scheme is https or wss 127 | var isTLSScheme: Bool { 128 | guard let scheme = self.scheme else { 129 | return false 130 | } 131 | return HTTPWSHeader.defaultSSLSchemes.contains(scheme) 132 | } 133 | 134 | /// getParts pulls host and port from the url. 135 | func getParts() -> URLParts? { 136 | guard let host = self.host else { 137 | return nil // no host, this isn't a valid url 138 | } 139 | let isTLS = isTLSScheme 140 | var port = self.port ?? 0 141 | if self.port == nil { 142 | if isTLS { 143 | port = 443 144 | } else { 145 | port = 80 146 | } 147 | } 148 | return URLParts(port: port, host: host, isTLS: isTLS) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /Sources/Starscream/WebSocket.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // Websocket.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 7/16/14. 7 | // Copyright (c) 2014-2019 Dalton Cherry. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | 25 | public enum ErrorType: Error { 26 | case compressionError 27 | case securityError 28 | case protocolError //There was an error parsing the WebSocket frames 29 | case serverError 30 | } 31 | 32 | public struct WSError: Error { 33 | public let type: ErrorType 34 | public let message: String 35 | public let code: UInt16 36 | 37 | public init(type: ErrorType, message: String, code: UInt16) { 38 | self.type = type 39 | self.message = message 40 | self.code = code 41 | } 42 | } 43 | 44 | public protocol WebSocketClient: AnyObject { 45 | func connect() 46 | func disconnect(closeCode: UInt16) 47 | func write(string: String, completion: (() -> ())?) 48 | func write(stringData: Data, completion: (() -> ())?) 49 | func write(data: Data, completion: (() -> ())?) 50 | func write(ping: Data, completion: (() -> ())?) 51 | func write(pong: Data, completion: (() -> ())?) 52 | } 53 | 54 | //implements some of the base behaviors 55 | extension WebSocketClient { 56 | public func write(string: String) { 57 | write(string: string, completion: nil) 58 | } 59 | 60 | public func write(data: Data) { 61 | write(data: data, completion: nil) 62 | } 63 | 64 | public func write(ping: Data) { 65 | write(ping: ping, completion: nil) 66 | } 67 | 68 | public func write(pong: Data) { 69 | write(pong: pong, completion: nil) 70 | } 71 | 72 | public func disconnect() { 73 | disconnect(closeCode: CloseCode.normal.rawValue) 74 | } 75 | } 76 | 77 | public enum WebSocketEvent { 78 | case connected([String: String]) 79 | case disconnected(String, UInt16) 80 | case text(String) 81 | case binary(Data) 82 | case pong(Data?) 83 | case ping(Data?) 84 | case error(Error?) 85 | case viabilityChanged(Bool) 86 | case reconnectSuggested(Bool) 87 | case cancelled 88 | case peerClosed 89 | } 90 | 91 | public protocol WebSocketDelegate: AnyObject { 92 | func didReceive(event: WebSocketEvent, client: WebSocketClient) 93 | } 94 | 95 | open class WebSocket: WebSocketClient, EngineDelegate { 96 | private let engine: Engine 97 | public weak var delegate: WebSocketDelegate? 98 | public var onEvent: ((WebSocketEvent) -> Void)? 99 | 100 | public var request: URLRequest 101 | // Where the callback is executed. It defaults to the main UI thread queue. 102 | public var callbackQueue = DispatchQueue.main 103 | public var respondToPingWithPong: Bool { 104 | set { 105 | guard let e = engine as? WSEngine else { return } 106 | e.respondToPingWithPong = newValue 107 | } 108 | get { 109 | guard let e = engine as? WSEngine else { return true } 110 | return e.respondToPingWithPong 111 | } 112 | } 113 | 114 | public init(request: URLRequest, engine: Engine) { 115 | self.request = request 116 | self.engine = engine 117 | } 118 | 119 | public convenience init(request: URLRequest, certPinner: CertificatePinning? = FoundationSecurity(), compressionHandler: CompressionHandler? = nil, useCustomEngine: Bool = true) { 120 | if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *), !useCustomEngine { 121 | self.init(request: request, engine: NativeEngine()) 122 | } else if #available(macOS 10.14, iOS 12.0, watchOS 5.0, tvOS 12.0, *) { 123 | self.init(request: request, engine: WSEngine(transport: TCPTransport(), certPinner: certPinner, compressionHandler: compressionHandler)) 124 | } else { 125 | self.init(request: request, engine: WSEngine(transport: FoundationTransport(), certPinner: certPinner, compressionHandler: compressionHandler)) 126 | } 127 | } 128 | 129 | public func connect() { 130 | engine.register(delegate: self) 131 | engine.start(request: request) 132 | } 133 | 134 | public func disconnect(closeCode: UInt16 = CloseCode.normal.rawValue) { 135 | engine.stop(closeCode: closeCode) 136 | } 137 | 138 | public func forceDisconnect() { 139 | engine.forceStop() 140 | } 141 | 142 | public func write(data: Data, completion: (() -> ())?) { 143 | write(data: data, opcode: .binaryFrame, completion: completion) 144 | } 145 | 146 | public func write(string: String, completion: (() -> ())?) { 147 | engine.write(string: string, completion: completion) 148 | } 149 | 150 | public func write(stringData: Data, completion: (() -> ())?) { 151 | write(data: stringData, opcode: .textFrame, completion: completion) 152 | } 153 | 154 | public func write(ping: Data, completion: (() -> ())?) { 155 | write(data: ping, opcode: .ping, completion: completion) 156 | } 157 | 158 | public func write(pong: Data, completion: (() -> ())?) { 159 | write(data: pong, opcode: .pong, completion: completion) 160 | } 161 | 162 | private func write(data: Data, opcode: FrameOpCode, completion: (() -> ())?) { 163 | engine.write(data: data, opcode: opcode, completion: completion) 164 | } 165 | 166 | // MARK: - EngineDelegate 167 | public func didReceive(event: WebSocketEvent) { 168 | callbackQueue.async { [weak self] in 169 | guard let s = self else { return } 170 | s.delegate?.didReceive(event: event, client: s) 171 | s.onEvent?(event) 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Sources/Transport/TCPTransport.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // HTTPTransport.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 1/23/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | #if canImport(Network) 24 | import Foundation 25 | import Network 26 | 27 | public enum TCPTransportError: Error { 28 | case invalidRequest 29 | } 30 | 31 | @available(macOS 10.14, iOS 12.0, watchOS 5.0, tvOS 12.0, *) 32 | public class TCPTransport: Transport { 33 | private var connection: NWConnection? 34 | private let queue = DispatchQueue(label: "com.vluxe.starscream.networkstream", attributes: []) 35 | private weak var delegate: TransportEventClient? 36 | private var isRunning = false 37 | private var isTLS = false 38 | 39 | deinit { 40 | disconnect() 41 | } 42 | 43 | public var usingTLS: Bool { 44 | return self.isTLS 45 | } 46 | 47 | public init(connection: NWConnection) { 48 | self.connection = connection 49 | start() 50 | } 51 | 52 | public init() { 53 | //normal connection, will use the "connect" method below 54 | } 55 | 56 | public func connect(url: URL, timeout: Double = 10, certificatePinning: CertificatePinning? = nil) { 57 | guard let parts = url.getParts() else { 58 | delegate?.connectionChanged(state: .failed(TCPTransportError.invalidRequest)) 59 | return 60 | } 61 | self.isTLS = parts.isTLS 62 | let options = NWProtocolTCP.Options() 63 | options.connectionTimeout = Int(timeout.rounded(.up)) 64 | 65 | let tlsOptions = isTLS ? NWProtocolTLS.Options() : nil 66 | if let tlsOpts = tlsOptions { 67 | sec_protocol_options_set_verify_block(tlsOpts.securityProtocolOptions, { (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in 68 | let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue() 69 | guard let pinner = certificatePinning else { 70 | sec_protocol_verify_complete(true) 71 | return 72 | } 73 | pinner.evaluateTrust(trust: trust, domain: parts.host, completion: { (state) in 74 | switch state { 75 | case .success: 76 | sec_protocol_verify_complete(true) 77 | case .failed(_): 78 | sec_protocol_verify_complete(false) 79 | } 80 | }) 81 | }, queue) 82 | } 83 | let parameters = NWParameters(tls: tlsOptions, tcp: options) 84 | let conn = NWConnection(host: NWEndpoint.Host.name(parts.host, nil), port: NWEndpoint.Port(rawValue: UInt16(parts.port))!, using: parameters) 85 | connection = conn 86 | start() 87 | } 88 | 89 | public func disconnect() { 90 | isRunning = false 91 | connection?.cancel() 92 | connection = nil 93 | } 94 | 95 | public func register(delegate: TransportEventClient) { 96 | self.delegate = delegate 97 | } 98 | 99 | public func write(data: Data, completion: @escaping ((Error?) -> ())) { 100 | connection?.send(content: data, completion: .contentProcessed { (error) in 101 | completion(error) 102 | }) 103 | } 104 | 105 | private func start() { 106 | guard let conn = connection else { 107 | return 108 | } 109 | conn.stateUpdateHandler = { [weak self] (newState) in 110 | switch newState { 111 | case .ready: 112 | self?.delegate?.connectionChanged(state: .connected) 113 | case .waiting: 114 | self?.delegate?.connectionChanged(state: .waiting) 115 | case .cancelled: 116 | self?.delegate?.connectionChanged(state: .cancelled) 117 | case .failed(let error): 118 | self?.delegate?.connectionChanged(state: .failed(error)) 119 | case .setup, .preparing: 120 | break 121 | @unknown default: 122 | break 123 | } 124 | } 125 | 126 | conn.viabilityUpdateHandler = { [weak self] (isViable) in 127 | self?.delegate?.connectionChanged(state: .viability(isViable)) 128 | } 129 | 130 | conn.betterPathUpdateHandler = { [weak self] (isBetter) in 131 | self?.delegate?.connectionChanged(state: .shouldReconnect(isBetter)) 132 | } 133 | 134 | conn.start(queue: queue) 135 | isRunning = true 136 | readLoop() 137 | } 138 | 139 | //readLoop keeps reading from the connection to get the latest content 140 | private func readLoop() { 141 | if !isRunning { 142 | return 143 | } 144 | connection?.receive(minimumIncompleteLength: 2, maximumLength: 4096, completion: {[weak self] (data, context, isComplete, error) in 145 | guard let s = self else {return} 146 | if let data = data { 147 | s.delegate?.connectionChanged(state: .receive(data)) 148 | } 149 | 150 | // Refer to https://developer.apple.com/documentation/network/implementing_netcat_with_network_framework 151 | if let context = context, context.isFinal, isComplete { 152 | if let delegate = s.delegate { 153 | // Let the owner of this TCPTransport decide what to do next: disconnect or reconnect? 154 | delegate.connectionChanged(state: .peerClosed) 155 | } else { 156 | // No use to keep connection alive 157 | s.disconnect() 158 | } 159 | return 160 | } 161 | 162 | if error == nil { 163 | s.readLoop() 164 | } 165 | 166 | }) 167 | } 168 | } 169 | #else 170 | typealias TCPTransport = FoundationTransport 171 | #endif 172 | -------------------------------------------------------------------------------- /examples/AutobahnTest/Autobahn/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Autobahn 4 | // 5 | // Created by Dalton Cherry on 7/24/15. 6 | // Copyright (c) 2015 vluxe. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Starscream 11 | 12 | class ViewController: UIViewController { 13 | 14 | let host = "localhost:9001" 15 | var socketArray = [WebSocket]() 16 | var caseCount = 300 //starting cases 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | getCaseCount() 20 | //getTestInfo(1) 21 | //runTest(304) 22 | } 23 | 24 | func removeSocket(_ s: WebSocket?) { 25 | guard let s = s else {return} 26 | socketArray = socketArray.filter{$0 !== s} 27 | } 28 | 29 | func getCaseCount() { 30 | let req = URLRequest(url: URL(string: "ws://\(host)/getCaseCount")!) 31 | let s = WebSocket(request: req) 32 | socketArray.append(s) 33 | s.onEvent = { [weak self] event in 34 | switch event { 35 | case .text(let string): 36 | if let c = Int(string) { 37 | print("number of cases is: \(c)") 38 | self?.caseCount = c 39 | } 40 | case .disconnected(_, _): 41 | self?.runTest(1) 42 | self?.removeSocket(s) 43 | default: 44 | break 45 | } 46 | } 47 | s.connect() 48 | } 49 | 50 | func getTestInfo(_ caseNum: Int) { 51 | let s = createSocket("getCaseInfo",caseNum) 52 | socketArray.append(s) 53 | // s.onText = { (text: String) in 54 | // let data = text.dataUsingEncoding(NSUTF8StringEncoding) 55 | // do { 56 | // let resp: AnyObject? = try NSJSONSerialization.JSONObjectWithData(data!, 57 | // options: NSJSONReadingOptions()) 58 | // if let dict = resp as? Dictionary { 59 | // let num = dict["id"] 60 | // let summary = dict["description"] 61 | // if let n = num, let sum = summary { 62 | // print("running case:\(caseNum) id:\(n) summary: \(sum)") 63 | // } 64 | // } 65 | // } catch { 66 | // print("error parsing the json") 67 | // } 68 | 69 | // } 70 | var once = false 71 | s.onEvent = { [weak self] event in 72 | switch event { 73 | case .disconnected(_, _), .error(_): 74 | if !once { 75 | once = true 76 | self?.runTest(caseNum) 77 | } 78 | self?.removeSocket(s) 79 | default: 80 | break 81 | } 82 | } 83 | s.connect() 84 | } 85 | 86 | func runTest(_ caseNum: Int) { 87 | let s = createSocket("runCase",caseNum) 88 | self.socketArray.append(s) 89 | 90 | var once = false 91 | s.onEvent = { [weak self, weak s] event in 92 | switch event { 93 | case .disconnected(_, _), .error(_): 94 | if !once { 95 | once = true 96 | print("case:\(caseNum) finished") 97 | //self?.verifyTest(caseNum) //disabled since it slows down the tests 98 | let nextCase = caseNum+1 99 | if nextCase <= (self?.caseCount)! { 100 | self?.runTest(nextCase) 101 | //self?.getTestInfo(nextCase) //disabled since it slows down the tests 102 | } else { 103 | self?.finishReports() 104 | } 105 | self?.removeSocket(s) 106 | } 107 | self?.removeSocket(s) 108 | case .text(let string): 109 | s?.write(string: string) 110 | case .binary(let data): 111 | s?.write(data: data) 112 | // case .error(let error): 113 | // print("got an error: \(error)") 114 | default: 115 | break 116 | } 117 | } 118 | s.connect() 119 | } 120 | 121 | // func verifyTest(_ caseNum: Int) { 122 | // let s = createSocket("getCaseStatus",caseNum) 123 | // self.socketArray.append(s) 124 | // s.onText = { (text: String) in 125 | // let data = text.data(using: String.Encoding.utf8) 126 | // do { 127 | // let resp: Any? = try JSONSerialization.jsonObject(with: data!, 128 | // options: JSONSerialization.ReadingOptions()) 129 | // if let dict = resp as? Dictionary { 130 | // if let status = dict["behavior"] { 131 | // if status == "OK" { 132 | // print("SUCCESS: \(caseNum)") 133 | // return 134 | // } 135 | // } 136 | // print("FAILURE: \(caseNum)") 137 | // } 138 | // } catch { 139 | // print("error parsing the json") 140 | // } 141 | // } 142 | // var once = false 143 | // s.onDisconnect = { [weak self, weak s] (error: Error?) in 144 | // if !once { 145 | // once = true 146 | // let nextCase = caseNum+1 147 | // print("next test is: \(nextCase)") 148 | // if nextCase <= (self?.caseCount)! { 149 | // self?.getTestInfo(nextCase) 150 | // } else { 151 | // self?.finishReports() 152 | // } 153 | // } 154 | // self?.removeSocket(s) 155 | // } 156 | // s.connect() 157 | // } 158 | 159 | func finishReports() { 160 | let s = createSocket("updateReports",0) 161 | self.socketArray.append(s) 162 | s.onEvent = { [weak self, weak s] event in 163 | switch event { 164 | case .disconnected(_, _): 165 | print("finished all the tests!") 166 | self?.removeSocket(s) 167 | default: 168 | break 169 | } 170 | } 171 | s.connect() 172 | } 173 | 174 | func createSocket(_ cmd: String, _ caseNum: Int) -> WebSocket { 175 | let req = URLRequest(url: URL(string: "ws://\(host)\(buildPath(cmd,caseNum))")!) 176 | //return WebSocket(request: req, compressionHandler: WSCompression()) 177 | return WebSocket(request: req) 178 | } 179 | 180 | func buildPath(_ cmd: String, _ caseNum: Int) -> String { 181 | return "/\(cmd)?case=\(caseNum)&agent=Starscream" 182 | } 183 | } 184 | 185 | -------------------------------------------------------------------------------- /Tests/FuzzingTests.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // FuzzingTests.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 1/28/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import XCTest 24 | @testable import Starscream 25 | 26 | class FuzzingTests: XCTestCase { 27 | 28 | var websocket: WebSocket! 29 | var server: MockServer! 30 | var uuid = "" 31 | 32 | override func setUp() { 33 | super.setUp() 34 | 35 | let s = MockServer() 36 | let _ = s.start(address: "", port: 0) 37 | server = s 38 | 39 | let transport = MockTransport(server: s) 40 | uuid = transport.uuid 41 | 42 | let url = URL(string: "http://vluxe.io/ws")! //domain doesn't matter with the mock transport 43 | let request = URLRequest(url: url) 44 | websocket = WebSocket(request: request, engine: WSEngine(transport: transport)) 45 | 46 | } 47 | 48 | override func tearDown() { 49 | super.tearDown() 50 | } 51 | 52 | func runWebsocket(timeout: TimeInterval = 10, serverAction: @escaping ((ServerEvent) -> Bool)) { 53 | let e = expectation(description: "Websocket event timeout") 54 | server.onEvent = { event in 55 | let done = serverAction(event) 56 | if done { 57 | e.fulfill() 58 | } 59 | } 60 | 61 | websocket.onEvent = { event in 62 | switch event { 63 | case .text(let string): 64 | self.websocket.write(string: string) 65 | case .binary(let data): 66 | self.websocket.write(data: data) 67 | case .ping(_): 68 | break 69 | case .pong(_): 70 | break 71 | case .connected(_): 72 | break 73 | case .disconnected(let reason, let code): 74 | print("reason: \(reason) code: \(code)") 75 | case .error(_): 76 | break 77 | case .viabilityChanged(_): 78 | break 79 | case .reconnectSuggested(_): 80 | break 81 | case .cancelled: 82 | break 83 | case .peerClosed: 84 | break 85 | } 86 | } 87 | websocket.connect() 88 | waitForExpectations(timeout: timeout) { error in 89 | if let error = error { 90 | XCTFail("waitForExpectationsWithTimeout errored: \(error)") 91 | } 92 | } 93 | } 94 | 95 | func sendMessage(string: String, isBinary: Bool) { 96 | let payload = string.data(using: .utf8)! 97 | let code: FrameOpCode = isBinary ? .binaryFrame : .textFrame 98 | runWebsocket { event in 99 | switch event { 100 | case .connected(let conn, _): 101 | conn.write(data: payload, opcode: code) 102 | case .text(let conn, let text): 103 | if text == string && !isBinary { 104 | conn.write(data: Data(), opcode: .connectionClose) 105 | return true //success! 106 | } else { 107 | XCTFail("text does not match: source: [\(string)] response: [\(text)]") 108 | } 109 | case .binary(let conn, let data): 110 | if payload.count == data.count && isBinary { 111 | conn.write(data: Data(), opcode: .connectionClose) 112 | return true //success! 113 | } else { 114 | XCTFail("binary does not match: source: [\(payload.count)] response: [\(data.count)]") 115 | } 116 | case .disconnected(_, _, _): 117 | return false 118 | default: 119 | XCTFail("recieved unexpected server event: \(event)") 120 | } 121 | return false 122 | } 123 | } 124 | 125 | //These are the Autobahn test cases as unit tests 126 | 127 | 128 | /// MARK : - Framing cases 129 | 130 | // case 1.1.1 131 | func testCase1() { 132 | sendMessage(string: "", isBinary: false) 133 | } 134 | 135 | // case 1.1.2 136 | func testCase2() { 137 | sendMessage(string: String(repeating: "*", count: 125), isBinary: false) 138 | } 139 | 140 | // case 1.1.3 141 | func testCase3() { 142 | sendMessage(string: String(repeating: "*", count: 126), isBinary: false) 143 | } 144 | 145 | // case 1.1.4 146 | func testCase4() { 147 | sendMessage(string: String(repeating: "*", count: 127), isBinary: false) 148 | } 149 | 150 | // case 1.1.5 151 | func testCase5() { 152 | sendMessage(string: String(repeating: "*", count: 128), isBinary: false) 153 | } 154 | 155 | // case 1.1.6 156 | func testCase6() { 157 | sendMessage(string: String(repeating: "*", count: 65535), isBinary: false) 158 | } 159 | 160 | // case 1.1.7, 1.1.8 161 | func testCase7() { 162 | sendMessage(string: String(repeating: "*", count: 65536), isBinary: false) 163 | } 164 | 165 | // case 1.2.1 166 | func testCase9() { 167 | sendMessage(string: "", isBinary: true) 168 | } 169 | 170 | // case 1.2.2 171 | func testCase10() { 172 | sendMessage(string: String(repeating: "*", count: 125), isBinary: true) 173 | } 174 | 175 | // case 1.2.3 176 | func testCase11() { 177 | sendMessage(string: String(repeating: "*", count: 126), isBinary: true) 178 | } 179 | 180 | // case 1.2.4 181 | func testCase12() { 182 | sendMessage(string: String(repeating: "*", count: 127), isBinary: true) 183 | } 184 | 185 | // case 1.2.5 186 | func testCase13() { 187 | sendMessage(string: String(repeating: "*", count: 128), isBinary: true) 188 | } 189 | 190 | // case 1.2.6 191 | func testCase14() { 192 | sendMessage(string: String(repeating: "*", count: 65535), isBinary: true) 193 | } 194 | 195 | // case 1.2.7, 1.2.8 196 | func testCase15() { 197 | sendMessage(string: String(repeating: "*", count: 65536), isBinary: true) 198 | } 199 | 200 | //TODO: the rest of them. 201 | } 202 | -------------------------------------------------------------------------------- /examples/WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 7 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 8 | # frameworks to, so exit 0 (signalling the script phase was successful). 9 | exit 0 10 | fi 11 | 12 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 13 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 14 | 15 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 16 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 17 | 18 | # Used as a return value for each invocation of `strip_invalid_archs` function. 19 | STRIP_BINARY_RETVAL=0 20 | 21 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 22 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 23 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 24 | 25 | # Copies and strips a vendored framework 26 | install_framework() 27 | { 28 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 29 | local source="${BUILT_PRODUCTS_DIR}/$1" 30 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 31 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 32 | elif [ -r "$1" ]; then 33 | local source="$1" 34 | fi 35 | 36 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 37 | 38 | if [ -L "${source}" ]; then 39 | echo "Symlinked..." 40 | source="$(readlink "${source}")" 41 | fi 42 | 43 | # Use filter instead of exclude so missing patterns don't throw errors. 44 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 45 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 46 | 47 | local basename 48 | basename="$(basename -s .framework "$1")" 49 | binary="${destination}/${basename}.framework/${basename}" 50 | 51 | if ! [ -r "$binary" ]; then 52 | binary="${destination}/${basename}" 53 | elif [ -L "${binary}" ]; then 54 | echo "Destination binary is symlinked..." 55 | dirname="$(dirname "${binary}")" 56 | binary="${dirname}/$(readlink "${binary}")" 57 | fi 58 | 59 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 60 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 61 | strip_invalid_archs "$binary" 62 | fi 63 | 64 | # Resign the code if required by the build settings to avoid unstable apps 65 | code_sign_if_enabled "${destination}/$(basename "$1")" 66 | 67 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 68 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 69 | local swift_runtime_libs 70 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u && exit ${PIPESTATUS[0]}) 71 | for lib in $swift_runtime_libs; do 72 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 73 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 74 | code_sign_if_enabled "${destination}/${lib}" 75 | done 76 | fi 77 | } 78 | 79 | # Copies and strips a vendored dSYM 80 | install_dsym() { 81 | local source="$1" 82 | if [ -r "$source" ]; then 83 | # Copy the dSYM into a the targets temp dir. 84 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 85 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 86 | 87 | local basename 88 | basename="$(basename -s .framework.dSYM "$source")" 89 | binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" 90 | 91 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 92 | if [[ "$(file "$binary")" == *"Mach-O dSYM companion"* ]]; then 93 | strip_invalid_archs "$binary" 94 | fi 95 | 96 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 97 | # Move the stripped file into its final destination. 98 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 99 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 100 | else 101 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 102 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" 103 | fi 104 | fi 105 | } 106 | 107 | # Signs a framework with the provided identity 108 | code_sign_if_enabled() { 109 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 110 | # Use the current code_sign_identity 111 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 112 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 113 | 114 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 115 | code_sign_cmd="$code_sign_cmd &" 116 | fi 117 | echo "$code_sign_cmd" 118 | eval "$code_sign_cmd" 119 | fi 120 | } 121 | 122 | # Strip invalid architectures 123 | strip_invalid_archs() { 124 | binary="$1" 125 | # Get architectures for current target binary 126 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 127 | # Intersect them with the architectures we are building for 128 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 129 | # If there are no archs supported by this binary then warn the user 130 | if [[ -z "$intersected_archs" ]]; then 131 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 132 | STRIP_BINARY_RETVAL=0 133 | return 134 | fi 135 | stripped="" 136 | for arch in $binary_archs; do 137 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 138 | # Strip non-valid architectures in-place 139 | lipo -remove "$arch" -output "$binary" "$binary" || exit 1 140 | stripped="$stripped $arch" 141 | fi 142 | done 143 | if [[ "$stripped" ]]; then 144 | echo "Stripped $binary of architectures:$stripped" 145 | fi 146 | STRIP_BINARY_RETVAL=1 147 | } 148 | 149 | 150 | if [[ "$CONFIGURATION" == "Debug" ]]; then 151 | install_framework "${BUILT_PRODUCTS_DIR}/Starscream/Starscream.framework" 152 | fi 153 | if [[ "$CONFIGURATION" == "Release" ]]; then 154 | install_framework "${BUILT_PRODUCTS_DIR}/Starscream/Starscream.framework" 155 | fi 156 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 157 | wait 158 | fi 159 | -------------------------------------------------------------------------------- /Sources/Server/WebSocketServer.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // WebSocketServer.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 4/5/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | #if canImport(Network) 24 | import Foundation 25 | import Network 26 | 27 | /// WebSocketServer is a Network.framework implementation of a WebSocket server 28 | @available(watchOS, unavailable) 29 | @available(macOS 10.14, iOS 12.0, watchOS 5.0, tvOS 12.0, *) 30 | public class WebSocketServer: Server, ConnectionDelegate { 31 | public var onEvent: ((ServerEvent) -> Void)? 32 | private var connections = [String: ServerConnection]() 33 | private var listener: NWListener? 34 | private let queue = DispatchQueue(label: "com.vluxe.starscream.server.networkstream", attributes: []) 35 | 36 | public init() { 37 | 38 | } 39 | 40 | public func start(address: String, port: UInt16) -> Error? { 41 | //TODO: support TLS cert adding/binding 42 | let parameters = NWParameters(tls: nil, tcp: NWProtocolTCP.Options()) 43 | let p = NWEndpoint.Port(rawValue: port)! 44 | parameters.requiredLocalEndpoint = NWEndpoint.hostPort(host: NWEndpoint.Host.name(address, nil), port: p) 45 | 46 | guard let listener = try? NWListener(using: parameters, on: p) else { 47 | return WSError(type: .serverError, message: "unable to start the listener at: \(address):\(port)", code: 0) 48 | } 49 | listener.newConnectionHandler = {[weak self] conn in 50 | let transport = TCPTransport(connection: conn) 51 | let c = ServerConnection(transport: transport) 52 | c.delegate = self 53 | self?.connections[c.uuid] = c 54 | } 55 | // listener.stateUpdateHandler = { state in 56 | // switch state { 57 | // case .ready: 58 | // print("ready to get sockets!") 59 | // case .setup: 60 | // print("setup to get sockets!") 61 | // case .cancelled: 62 | // print("server cancelled!") 63 | // case .waiting(let error): 64 | // print("waiting error: \(error)") 65 | // case .failed(let error): 66 | // print("server failed: \(error)") 67 | // @unknown default: 68 | // print("wat?") 69 | // } 70 | // } 71 | self.listener = listener 72 | listener.start(queue: queue) 73 | return nil 74 | } 75 | 76 | public func didReceive(event: ServerEvent) { 77 | onEvent?(event) 78 | switch event { 79 | case .disconnected(let conn, _, _): 80 | guard let conn = conn as? ServerConnection else { 81 | return 82 | } 83 | connections.removeValue(forKey: conn.uuid) 84 | default: 85 | break 86 | } 87 | } 88 | } 89 | 90 | @available(macOS 10.14, iOS 12.0, watchOS 5.0, tvOS 12.0, *) 91 | public class ServerConnection: Connection, HTTPServerDelegate, FramerEventClient, FrameCollectorDelegate, TransportEventClient { 92 | let transport: TCPTransport 93 | private let httpHandler = FoundationHTTPServerHandler() 94 | private let framer = WSFramer(isServer: true) 95 | private let frameHandler = FrameCollector() 96 | private var didUpgrade = false 97 | public var onEvent: ((ConnectionEvent) -> Void)? 98 | public weak var delegate: ConnectionDelegate? 99 | private let id: String 100 | var uuid: String { 101 | return id 102 | } 103 | 104 | init(transport: TCPTransport) { 105 | self.id = UUID().uuidString 106 | self.transport = transport 107 | transport.register(delegate: self) 108 | httpHandler.register(delegate: self) 109 | framer.register(delegate: self) 110 | frameHandler.delegate = self 111 | } 112 | 113 | public func write(data: Data, opcode: FrameOpCode) { 114 | let wsData = framer.createWriteFrame(opcode: opcode, payload: data, isCompressed: false) 115 | transport.write(data: wsData, completion: {_ in }) 116 | } 117 | 118 | // MARK: - TransportEventClient 119 | 120 | public func connectionChanged(state: ConnectionState) { 121 | switch state { 122 | case .connected: 123 | break 124 | case .waiting: 125 | break 126 | case .failed(let error): 127 | print("server connection error: \(error ?? WSError(type: .protocolError, message: "default error, no extra data", code: 0))") //handleError(error) 128 | case .viability(_): 129 | break 130 | case .shouldReconnect(_): 131 | break 132 | case .receive(let data): 133 | if didUpgrade { 134 | framer.add(data: data) 135 | } else { 136 | httpHandler.parse(data: data) 137 | } 138 | case .cancelled: 139 | print("server connection cancelled!") 140 | //broadcast(event: .cancelled) 141 | case .peerClosed: 142 | delegate?.didReceive(event: .disconnected(self, "Connection closed by peer", UInt16(FrameOpCode.connectionClose.rawValue))) 143 | } 144 | } 145 | 146 | /// MARK: - HTTPServerDelegate 147 | 148 | public func didReceive(event: HTTPEvent) { 149 | switch event { 150 | case .success(let headers): 151 | didUpgrade = true 152 | let response = httpHandler.createResponse(headers: [:]) 153 | transport.write(data: response, completion: {_ in }) 154 | delegate?.didReceive(event: .connected(self, headers)) 155 | onEvent?(.connected(headers)) 156 | case .failure(let error): 157 | onEvent?(.error(error)) 158 | } 159 | } 160 | 161 | /// MARK: - FrameCollectorDelegate 162 | 163 | public func frameProcessed(event: FrameEvent) { 164 | switch event { 165 | case .frame(let frame): 166 | frameHandler.add(frame: frame) 167 | case .error(let error): 168 | onEvent?(.error(error)) 169 | } 170 | } 171 | 172 | public func didForm(event: FrameCollector.Event) { 173 | switch event { 174 | case .text(let string): 175 | delegate?.didReceive(event: .text(self, string)) 176 | onEvent?(.text(string)) 177 | case .binary(let data): 178 | delegate?.didReceive(event: .binary(self, data)) 179 | onEvent?(.binary(data)) 180 | case .pong(let data): 181 | delegate?.didReceive(event: .pong(self, data)) 182 | onEvent?(.pong(data)) 183 | case .ping(let data): 184 | delegate?.didReceive(event: .ping(self, data)) 185 | onEvent?(.ping(data)) 186 | case .closed(let reason, let code): 187 | delegate?.didReceive(event: .disconnected(self, reason, code)) 188 | onEvent?(.disconnected(reason, code)) 189 | case .error(let error): 190 | onEvent?(.error(error)) 191 | } 192 | } 193 | 194 | public func decompress(data: Data, isFinal: Bool) -> Data? { 195 | return nil 196 | } 197 | } 198 | #endif 199 | -------------------------------------------------------------------------------- /Sources/Transport/FoundationTransport.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // FoundationTransport.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 1/23/19. 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | 25 | public enum FoundationTransportError: Error { 26 | case invalidRequest 27 | case invalidOutputStream 28 | case timeout 29 | } 30 | 31 | public class FoundationTransport: NSObject, Transport, StreamDelegate { 32 | private weak var delegate: TransportEventClient? 33 | private let workQueue = DispatchQueue(label: "com.vluxe.starscream.websocket", attributes: []) 34 | private var inputStream: InputStream? 35 | private var outputStream: OutputStream? 36 | private var isOpen = false 37 | private var onConnect: ((InputStream, OutputStream) -> Void)? 38 | private var isTLS = false 39 | private var certPinner: CertificatePinning? 40 | 41 | public var usingTLS: Bool { 42 | return self.isTLS 43 | } 44 | 45 | public init(streamConfiguration: ((InputStream, OutputStream) -> Void)? = nil) { 46 | super.init() 47 | onConnect = streamConfiguration 48 | } 49 | 50 | deinit { 51 | inputStream?.delegate = nil 52 | outputStream?.delegate = nil 53 | } 54 | 55 | public func connect(url: URL, timeout: Double = 10, certificatePinning: CertificatePinning? = nil) { 56 | guard let parts = url.getParts() else { 57 | delegate?.connectionChanged(state: .failed(FoundationTransportError.invalidRequest)) 58 | return 59 | } 60 | self.certPinner = certificatePinning 61 | self.isTLS = parts.isTLS 62 | var readStream: Unmanaged? 63 | var writeStream: Unmanaged? 64 | let h = parts.host as NSString 65 | CFStreamCreatePairWithSocketToHost(nil, h, UInt32(parts.port), &readStream, &writeStream) 66 | inputStream = readStream!.takeRetainedValue() 67 | outputStream = writeStream!.takeRetainedValue() 68 | guard let inStream = inputStream, let outStream = outputStream else { 69 | return 70 | } 71 | inStream.delegate = self 72 | outStream.delegate = self 73 | 74 | if isTLS { 75 | let key = CFStreamPropertyKey(rawValue: kCFStreamPropertySocketSecurityLevel) 76 | CFReadStreamSetProperty(inStream, key, kCFStreamSocketSecurityLevelNegotiatedSSL) 77 | CFWriteStreamSetProperty(outStream, key, kCFStreamSocketSecurityLevelNegotiatedSSL) 78 | } 79 | 80 | onConnect?(inStream, outStream) 81 | 82 | isOpen = false 83 | CFReadStreamSetDispatchQueue(inStream, workQueue) 84 | CFWriteStreamSetDispatchQueue(outStream, workQueue) 85 | inStream.open() 86 | outStream.open() 87 | 88 | 89 | workQueue.asyncAfter(deadline: .now() + timeout, execute: { [weak self] in 90 | guard let s = self else { return } 91 | if !s.isOpen { 92 | s.delegate?.connectionChanged(state: .failed(FoundationTransportError.timeout)) 93 | } 94 | }) 95 | } 96 | 97 | public func disconnect() { 98 | if let stream = inputStream { 99 | stream.delegate = nil 100 | CFReadStreamSetDispatchQueue(stream, nil) 101 | stream.close() 102 | } 103 | if let stream = outputStream { 104 | stream.delegate = nil 105 | CFWriteStreamSetDispatchQueue(stream, nil) 106 | stream.close() 107 | } 108 | isOpen = false 109 | outputStream = nil 110 | inputStream = nil 111 | } 112 | 113 | public func register(delegate: TransportEventClient) { 114 | self.delegate = delegate 115 | } 116 | 117 | public func write(data: Data, completion: @escaping ((Error?) -> ())) { 118 | guard let outStream = outputStream else { 119 | completion(FoundationTransportError.invalidOutputStream) 120 | return 121 | } 122 | var total = 0 123 | let buffer = UnsafeRawPointer((data as NSData).bytes).assumingMemoryBound(to: UInt8.self) 124 | //NOTE: this might need to be dispatched to the work queue instead of being written inline. TBD. 125 | while total < data.count { 126 | let written = outStream.write(buffer, maxLength: data.count) 127 | if written < 0 { 128 | completion(FoundationTransportError.invalidOutputStream) 129 | return 130 | } 131 | total += written 132 | } 133 | completion(nil) 134 | } 135 | 136 | private func getSecurityData() -> (SecTrust?, String?) { 137 | #if os(watchOS) 138 | return (nil, nil) 139 | #else 140 | guard let outputStream = outputStream else { 141 | return (nil, nil) 142 | } 143 | let trust = outputStream.property(forKey: kCFStreamPropertySSLPeerTrust as Stream.PropertyKey) as! SecTrust? 144 | var domain = outputStream.property(forKey: kCFStreamSSLPeerName as Stream.PropertyKey) as! String? 145 | 146 | if domain == nil, 147 | let sslContextOut = CFWriteStreamCopyProperty(outputStream, CFStreamPropertyKey(rawValue: kCFStreamPropertySSLContext)) as! SSLContext? { 148 | var peerNameLen: Int = 0 149 | SSLGetPeerDomainNameLength(sslContextOut, &peerNameLen) 150 | var peerName = Data(count: peerNameLen) 151 | let _ = peerName.withUnsafeMutableBytes { (peerNamePtr: UnsafeMutablePointer) in 152 | SSLGetPeerDomainName(sslContextOut, peerNamePtr, &peerNameLen) 153 | } 154 | if let peerDomain = String(bytes: peerName, encoding: .utf8), peerDomain.count > 0 { 155 | domain = peerDomain 156 | } 157 | } 158 | return (trust, domain) 159 | #endif 160 | } 161 | 162 | private func read() { 163 | guard let stream = inputStream else { 164 | return 165 | } 166 | let maxBuffer = 4096 167 | let buf = NSMutableData(capacity: maxBuffer) 168 | let buffer = UnsafeMutableRawPointer(mutating: buf!.bytes).assumingMemoryBound(to: UInt8.self) 169 | let length = stream.read(buffer, maxLength: maxBuffer) 170 | if length < 1 { 171 | return 172 | } 173 | let data = Data(bytes: buffer, count: length) 174 | delegate?.connectionChanged(state: .receive(data)) 175 | } 176 | 177 | // MARK: - StreamDelegate 178 | 179 | open func stream(_ aStream: Stream, handle eventCode: Stream.Event) { 180 | switch eventCode { 181 | case .hasBytesAvailable: 182 | if aStream == inputStream { 183 | read() 184 | } 185 | case .errorOccurred: 186 | delegate?.connectionChanged(state: .failed(aStream.streamError)) 187 | case .endEncountered: 188 | if aStream == inputStream { 189 | delegate?.connectionChanged(state: .cancelled) 190 | } 191 | case .openCompleted: 192 | if aStream == inputStream { 193 | let (trust, domain) = getSecurityData() 194 | if let pinner = certPinner, let trust = trust { 195 | pinner.evaluateTrust(trust: trust, domain: domain, completion: { [weak self] (state) in 196 | switch state { 197 | case .success: 198 | self?.isOpen = true 199 | self?.delegate?.connectionChanged(state: .connected) 200 | case .failed(let error): 201 | self?.delegate?.connectionChanged(state: .failed(error)) 202 | } 203 | 204 | }) 205 | } else { 206 | isOpen = true 207 | delegate?.connectionChanged(state: .connected) 208 | } 209 | } 210 | case .endEncountered: 211 | if aStream == inputStream { 212 | delegate?.connectionChanged(state: .cancelled) 213 | } 214 | default: 215 | break 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (6.1.7.7) 9 | concurrent-ruby (~> 1.0, >= 1.0.2) 10 | i18n (>= 1.6, < 2) 11 | minitest (>= 5.1) 12 | tzinfo (~> 2.0) 13 | zeitwerk (~> 2.3) 14 | addressable (2.8.6) 15 | public_suffix (>= 2.0.2, < 6.0) 16 | algoliasearch (1.27.5) 17 | httpclient (~> 2.8, >= 2.8.3) 18 | json (>= 1.5.1) 19 | artifactory (3.0.17) 20 | atomos (0.1.3) 21 | aws-eventstream (1.3.0) 22 | aws-partitions (1.895.0) 23 | aws-sdk-core (3.191.3) 24 | aws-eventstream (~> 1, >= 1.3.0) 25 | aws-partitions (~> 1, >= 1.651.0) 26 | aws-sigv4 (~> 1.8) 27 | jmespath (~> 1, >= 1.6.1) 28 | aws-sdk-kms (1.77.0) 29 | aws-sdk-core (~> 3, >= 3.191.0) 30 | aws-sigv4 (~> 1.1) 31 | aws-sdk-s3 (1.143.0) 32 | aws-sdk-core (~> 3, >= 3.191.0) 33 | aws-sdk-kms (~> 1) 34 | aws-sigv4 (~> 1.8) 35 | aws-sigv4 (1.8.0) 36 | aws-eventstream (~> 1, >= 1.0.2) 37 | babosa (1.0.4) 38 | base64 (0.2.0) 39 | claide (1.1.0) 40 | cocoapods (1.15.2) 41 | addressable (~> 2.8) 42 | claide (>= 1.0.2, < 2.0) 43 | cocoapods-core (= 1.15.2) 44 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 45 | cocoapods-downloader (>= 2.1, < 3.0) 46 | cocoapods-plugins (>= 1.0.0, < 2.0) 47 | cocoapods-search (>= 1.0.0, < 2.0) 48 | cocoapods-trunk (>= 1.6.0, < 2.0) 49 | cocoapods-try (>= 1.1.0, < 2.0) 50 | colored2 (~> 3.1) 51 | escape (~> 0.0.4) 52 | fourflusher (>= 2.3.0, < 3.0) 53 | gh_inspector (~> 1.0) 54 | molinillo (~> 0.8.0) 55 | nap (~> 1.0) 56 | ruby-macho (>= 2.3.0, < 3.0) 57 | xcodeproj (>= 1.23.0, < 2.0) 58 | cocoapods-core (1.15.2) 59 | activesupport (>= 5.0, < 8) 60 | addressable (~> 2.8) 61 | algoliasearch (~> 1.0) 62 | concurrent-ruby (~> 1.1) 63 | fuzzy_match (~> 2.0.4) 64 | nap (~> 1.0) 65 | netrc (~> 0.11) 66 | public_suffix (~> 4.0) 67 | typhoeus (~> 1.0) 68 | cocoapods-deintegrate (1.0.5) 69 | cocoapods-downloader (2.1) 70 | cocoapods-plugins (1.0.0) 71 | nap 72 | cocoapods-search (1.0.1) 73 | cocoapods-trunk (1.6.0) 74 | nap (>= 0.8, < 2.0) 75 | netrc (~> 0.11) 76 | cocoapods-try (1.2.0) 77 | colored (1.2) 78 | colored2 (3.1.2) 79 | commander (4.6.0) 80 | highline (~> 2.0.0) 81 | concurrent-ruby (1.2.3) 82 | declarative (0.0.20) 83 | digest-crc (0.6.5) 84 | rake (>= 12.0.0, < 14.0.0) 85 | domain_name (0.5.20190701) 86 | unf (>= 0.0.5, < 1.0.0) 87 | dotenv (2.8.1) 88 | emoji_regex (3.2.3) 89 | escape (0.0.4) 90 | ethon (0.16.0) 91 | ffi (>= 1.15.0) 92 | excon (0.109.0) 93 | faraday (1.10.3) 94 | faraday-em_http (~> 1.0) 95 | faraday-em_synchrony (~> 1.0) 96 | faraday-excon (~> 1.1) 97 | faraday-httpclient (~> 1.0) 98 | faraday-multipart (~> 1.0) 99 | faraday-net_http (~> 1.0) 100 | faraday-net_http_persistent (~> 1.0) 101 | faraday-patron (~> 1.0) 102 | faraday-rack (~> 1.0) 103 | faraday-retry (~> 1.0) 104 | ruby2_keywords (>= 0.0.4) 105 | faraday-cookie_jar (0.0.7) 106 | faraday (>= 0.8.0) 107 | http-cookie (~> 1.0.0) 108 | faraday-em_http (1.0.0) 109 | faraday-em_synchrony (1.0.0) 110 | faraday-excon (1.1.0) 111 | faraday-httpclient (1.0.1) 112 | faraday-multipart (1.0.4) 113 | multipart-post (~> 2) 114 | faraday-net_http (1.0.1) 115 | faraday-net_http_persistent (1.2.0) 116 | faraday-patron (1.0.0) 117 | faraday-rack (1.0.0) 118 | faraday-retry (1.0.3) 119 | faraday_middleware (1.2.0) 120 | faraday (~> 1.0) 121 | fastimage (2.3.0) 122 | fastlane (2.219.0) 123 | CFPropertyList (>= 2.3, < 4.0.0) 124 | addressable (>= 2.8, < 3.0.0) 125 | artifactory (~> 3.0) 126 | aws-sdk-s3 (~> 1.0) 127 | babosa (>= 1.0.3, < 2.0.0) 128 | bundler (>= 1.12.0, < 3.0.0) 129 | colored 130 | commander (~> 4.6) 131 | dotenv (>= 2.1.1, < 3.0.0) 132 | emoji_regex (>= 0.1, < 4.0) 133 | excon (>= 0.71.0, < 1.0.0) 134 | faraday (~> 1.0) 135 | faraday-cookie_jar (~> 0.0.6) 136 | faraday_middleware (~> 1.0) 137 | fastimage (>= 2.1.0, < 3.0.0) 138 | gh_inspector (>= 1.1.2, < 2.0.0) 139 | google-apis-androidpublisher_v3 (~> 0.3) 140 | google-apis-playcustomapp_v1 (~> 0.1) 141 | google-cloud-env (>= 1.6.0, < 2.0.0) 142 | google-cloud-storage (~> 1.31) 143 | highline (~> 2.0) 144 | http-cookie (~> 1.0.5) 145 | json (< 3.0.0) 146 | jwt (>= 2.1.0, < 3) 147 | mini_magick (>= 4.9.4, < 5.0.0) 148 | multipart-post (>= 2.0.0, < 3.0.0) 149 | naturally (~> 2.2) 150 | optparse (>= 0.1.1) 151 | plist (>= 3.1.0, < 4.0.0) 152 | rubyzip (>= 2.0.0, < 3.0.0) 153 | security (= 0.1.3) 154 | simctl (~> 1.6.3) 155 | terminal-notifier (>= 2.0.0, < 3.0.0) 156 | terminal-table (~> 3) 157 | tty-screen (>= 0.6.3, < 1.0.0) 158 | tty-spinner (>= 0.8.0, < 1.0.0) 159 | word_wrap (~> 1.0.0) 160 | xcodeproj (>= 1.13.0, < 2.0.0) 161 | xcpretty (~> 0.3.0) 162 | xcpretty-travis-formatter (>= 0.0.3) 163 | ffi (1.16.3) 164 | fourflusher (2.3.1) 165 | fuzzy_match (2.0.4) 166 | gh_inspector (1.1.3) 167 | google-apis-androidpublisher_v3 (0.54.0) 168 | google-apis-core (>= 0.11.0, < 2.a) 169 | google-apis-core (0.11.3) 170 | addressable (~> 2.5, >= 2.5.1) 171 | googleauth (>= 0.16.2, < 2.a) 172 | httpclient (>= 2.8.1, < 3.a) 173 | mini_mime (~> 1.0) 174 | representable (~> 3.0) 175 | retriable (>= 2.0, < 4.a) 176 | rexml 177 | google-apis-iamcredentials_v1 (0.17.0) 178 | google-apis-core (>= 0.11.0, < 2.a) 179 | google-apis-playcustomapp_v1 (0.13.0) 180 | google-apis-core (>= 0.11.0, < 2.a) 181 | google-apis-storage_v1 (0.29.0) 182 | google-apis-core (>= 0.11.0, < 2.a) 183 | google-cloud-core (1.6.1) 184 | google-cloud-env (>= 1.0, < 3.a) 185 | google-cloud-errors (~> 1.0) 186 | google-cloud-env (1.6.0) 187 | faraday (>= 0.17.3, < 3.0) 188 | google-cloud-errors (1.3.1) 189 | google-cloud-storage (1.45.0) 190 | addressable (~> 2.8) 191 | digest-crc (~> 0.4) 192 | google-apis-iamcredentials_v1 (~> 0.1) 193 | google-apis-storage_v1 (~> 0.29.0) 194 | google-cloud-core (~> 1.6) 195 | googleauth (>= 0.16.2, < 2.a) 196 | mini_mime (~> 1.0) 197 | googleauth (1.8.1) 198 | faraday (>= 0.17.3, < 3.a) 199 | jwt (>= 1.4, < 3.0) 200 | multi_json (~> 1.11) 201 | os (>= 0.9, < 2.0) 202 | signet (>= 0.16, < 2.a) 203 | highline (2.0.3) 204 | http-cookie (1.0.5) 205 | domain_name (~> 0.5) 206 | httpclient (2.8.3) 207 | i18n (1.14.4) 208 | concurrent-ruby (~> 1.0) 209 | jmespath (1.6.2) 210 | json (2.7.1) 211 | jwt (2.8.1) 212 | base64 213 | mini_magick (4.12.0) 214 | mini_mime (1.1.5) 215 | minitest (5.22.2) 216 | molinillo (0.8.0) 217 | multi_json (1.15.0) 218 | multipart-post (2.4.0) 219 | nanaimo (0.3.0) 220 | nap (1.1.0) 221 | naturally (2.2.1) 222 | netrc (0.11.0) 223 | nkf (0.2.0) 224 | optparse (0.4.0) 225 | os (1.1.4) 226 | plist (3.7.1) 227 | public_suffix (4.0.7) 228 | rake (13.1.0) 229 | representable (3.2.0) 230 | declarative (< 0.1.0) 231 | trailblazer-option (>= 0.1.1, < 0.2.0) 232 | uber (< 0.2.0) 233 | retriable (3.1.2) 234 | rexml (3.2.6) 235 | rouge (2.0.7) 236 | ruby-macho (2.5.1) 237 | ruby2_keywords (0.0.5) 238 | rubyzip (2.3.2) 239 | security (0.1.3) 240 | signet (0.18.0) 241 | addressable (~> 2.8) 242 | faraday (>= 0.17.5, < 3.a) 243 | jwt (>= 1.5, < 3.0) 244 | multi_json (~> 1.10) 245 | simctl (1.6.10) 246 | CFPropertyList 247 | naturally 248 | terminal-notifier (2.0.0) 249 | terminal-table (3.0.2) 250 | unicode-display_width (>= 1.1.1, < 3) 251 | trailblazer-option (0.1.2) 252 | tty-cursor (0.7.1) 253 | tty-screen (0.8.2) 254 | tty-spinner (0.9.3) 255 | tty-cursor (~> 0.7) 256 | typhoeus (1.4.1) 257 | ethon (>= 0.9.0) 258 | tzinfo (2.0.6) 259 | concurrent-ruby (~> 1.0) 260 | uber (0.1.0) 261 | unf (0.1.4) 262 | unf_ext 263 | unf_ext (0.0.9.1) 264 | unicode-display_width (2.5.0) 265 | word_wrap (1.0.0) 266 | xcodeproj (1.24.0) 267 | CFPropertyList (>= 2.3.3, < 4.0) 268 | atomos (~> 0.1.3) 269 | claide (>= 1.0.2, < 2.0) 270 | colored2 (~> 3.1) 271 | nanaimo (~> 0.3.0) 272 | rexml (~> 3.2.4) 273 | xcpretty (0.3.0) 274 | rouge (~> 2.0.7) 275 | xcpretty-travis-formatter (1.0.1) 276 | xcpretty (~> 0.2, >= 0.0.7) 277 | zeitwerk (2.6.13) 278 | 279 | PLATFORMS 280 | arm64-darwin-22 281 | arm64-darwin-23 282 | x86_64-darwin-20 283 | x86_64-linux 284 | 285 | DEPENDENCIES 286 | cocoapods 287 | fastlane 288 | 289 | BUNDLED WITH 290 | 2.4.19 291 | -------------------------------------------------------------------------------- /Sources/Compression/WSCompression.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // WSCompression.swift 4 | // 5 | // Created by Joseph Ross on 7/16/14. 6 | // Copyright © 2017 Joseph Ross & Vluxe. All rights reserved. 7 | // 8 | // Licensed under the Apache License, Version 2.0 (the "License"); 9 | // you may not use this file except in compliance with the License. 10 | // You may obtain a copy of the License at 11 | // 12 | // http://www.apache.org/licenses/LICENSE-2.0 13 | // 14 | // Unless required by applicable law or agreed to in writing, software 15 | // distributed under the License is distributed on an "AS IS" BASIS, 16 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | // See the License for the specific language governing permissions and 18 | // limitations under the License. 19 | // 20 | ////////////////////////////////////////////////////////////////////////////////////////////////// 21 | 22 | ////////////////////////////////////////////////////////////////////////////////////////////////// 23 | // 24 | // Compression implementation is implemented in conformance with RFC 7692 Compression Extensions 25 | // for WebSocket: https://tools.ietf.org/html/rfc7692 26 | // 27 | ////////////////////////////////////////////////////////////////////////////////////////////////// 28 | 29 | import Foundation 30 | import zlib 31 | 32 | public class WSCompression: CompressionHandler { 33 | let headerWSExtensionName = "Sec-WebSocket-Extensions" 34 | var decompressor: Decompressor? 35 | var compressor: Compressor? 36 | var decompressorTakeOver = false 37 | var compressorTakeOver = false 38 | 39 | public init() { 40 | 41 | } 42 | 43 | public func load(headers: [String: String]) { 44 | guard let extensionHeader = headers[headerWSExtensionName] else { return } 45 | decompressorTakeOver = false 46 | compressorTakeOver = false 47 | 48 | // assume defaults unless the headers say otherwise 49 | compressor = Compressor(windowBits: 15) 50 | decompressor = Decompressor(windowBits: 15) 51 | 52 | let parts = extensionHeader.components(separatedBy: ";") 53 | for p in parts { 54 | let part = p.trimmingCharacters(in: .whitespaces) 55 | if part.hasPrefix("server_max_window_bits=") { 56 | let valString = part.components(separatedBy: "=")[1] 57 | if let val = Int(valString.trimmingCharacters(in: .whitespaces)) { 58 | decompressor = Decompressor(windowBits: val) 59 | } 60 | } else if part.hasPrefix("client_max_window_bits=") { 61 | let valString = part.components(separatedBy: "=")[1] 62 | if let val = Int(valString.trimmingCharacters(in: .whitespaces)) { 63 | compressor = Compressor(windowBits: val) 64 | } 65 | } else if part == "client_no_context_takeover" { 66 | compressorTakeOver = true 67 | } else if part == "server_no_context_takeover" { 68 | decompressorTakeOver = true 69 | } 70 | } 71 | } 72 | 73 | public func decompress(data: Data, isFinal: Bool) -> Data? { 74 | guard let decompressor = decompressor else { return nil } 75 | do { 76 | let decompressedData = try decompressor.decompress(data, finish: isFinal) 77 | if decompressorTakeOver { 78 | try decompressor.reset() 79 | } 80 | return decompressedData 81 | } catch { 82 | //do nothing with the error for now 83 | } 84 | return nil 85 | } 86 | 87 | public func compress(data: Data) -> Data? { 88 | guard let compressor = compressor else { return nil } 89 | do { 90 | let compressedData = try compressor.compress(data) 91 | if compressorTakeOver { 92 | try compressor.reset() 93 | } 94 | return compressedData 95 | } catch { 96 | //do nothing with the error for now 97 | } 98 | return nil 99 | } 100 | 101 | 102 | } 103 | 104 | class Decompressor { 105 | private var strm = z_stream() 106 | private var buffer = [UInt8](repeating: 0, count: 0x2000) 107 | private var inflateInitialized = false 108 | private let windowBits: Int 109 | 110 | init?(windowBits: Int) { 111 | self.windowBits = windowBits 112 | guard initInflate() else { return nil } 113 | } 114 | 115 | private func initInflate() -> Bool { 116 | if Z_OK == inflateInit2_(&strm, -CInt(windowBits), 117 | ZLIB_VERSION, CInt(MemoryLayout.size)) 118 | { 119 | inflateInitialized = true 120 | return true 121 | } 122 | return false 123 | } 124 | 125 | func reset() throws { 126 | teardownInflate() 127 | guard initInflate() else { throw WSError(type: .compressionError, message: "Error for decompressor on reset", code: 0) } 128 | } 129 | 130 | func decompress(_ data: Data, finish: Bool) throws -> Data { 131 | return try data.withUnsafeBytes { (bytes: UnsafePointer) -> Data in 132 | return try decompress(bytes: bytes, count: data.count, finish: finish) 133 | } 134 | } 135 | 136 | func decompress(bytes: UnsafePointer, count: Int, finish: Bool) throws -> Data { 137 | var decompressed = Data() 138 | try decompress(bytes: bytes, count: count, out: &decompressed) 139 | 140 | if finish { 141 | let tail:[UInt8] = [0x00, 0x00, 0xFF, 0xFF] 142 | try decompress(bytes: tail, count: tail.count, out: &decompressed) 143 | } 144 | 145 | return decompressed 146 | } 147 | 148 | private func decompress(bytes: UnsafePointer, count: Int, out: inout Data) throws { 149 | var res: CInt = 0 150 | strm.next_in = UnsafeMutablePointer(mutating: bytes) 151 | strm.avail_in = CUnsignedInt(count) 152 | 153 | repeat { 154 | buffer.withUnsafeMutableBytes { (bufferPtr) in 155 | strm.next_out = bufferPtr.bindMemory(to: UInt8.self).baseAddress 156 | strm.avail_out = CUnsignedInt(bufferPtr.count) 157 | 158 | res = inflate(&strm, 0) 159 | } 160 | 161 | let byteCount = buffer.count - Int(strm.avail_out) 162 | out.append(buffer, count: byteCount) 163 | } while res == Z_OK && strm.avail_out == 0 164 | 165 | guard (res == Z_OK && strm.avail_out > 0) 166 | || (res == Z_BUF_ERROR && Int(strm.avail_out) == buffer.count) 167 | else { 168 | throw WSError(type: .compressionError, message: "Error on decompressing", code: 0) 169 | } 170 | } 171 | 172 | private func teardownInflate() { 173 | if inflateInitialized, Z_OK == inflateEnd(&strm) { 174 | inflateInitialized = false 175 | } 176 | } 177 | 178 | deinit { 179 | teardownInflate() 180 | } 181 | } 182 | 183 | class Compressor { 184 | private var strm = z_stream() 185 | private var buffer = [UInt8](repeating: 0, count: 0x2000) 186 | private var deflateInitialized = false 187 | private let windowBits: Int 188 | 189 | init?(windowBits: Int) { 190 | self.windowBits = windowBits 191 | guard initDeflate() else { return nil } 192 | } 193 | 194 | private func initDeflate() -> Bool { 195 | if Z_OK == deflateInit2_(&strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 196 | -CInt(windowBits), 8, Z_DEFAULT_STRATEGY, 197 | ZLIB_VERSION, CInt(MemoryLayout.size)) 198 | { 199 | deflateInitialized = true 200 | return true 201 | } 202 | return false 203 | } 204 | 205 | func reset() throws { 206 | teardownDeflate() 207 | guard initDeflate() else { throw WSError(type: .compressionError, message: "Error for compressor on reset", code: 0) } 208 | } 209 | 210 | func compress(_ data: Data) throws -> Data { 211 | guard !data.isEmpty else { 212 | // For example, PONG has no content 213 | return data 214 | } 215 | 216 | var compressed = Data() 217 | var res: CInt = 0 218 | data.withUnsafeBytes { (ptr:UnsafePointer) -> Void in 219 | strm.next_in = UnsafeMutablePointer(mutating: ptr) 220 | strm.avail_in = CUnsignedInt(data.count) 221 | 222 | repeat { 223 | buffer.withUnsafeMutableBytes { (bufferPtr) in 224 | strm.next_out = bufferPtr.bindMemory(to: UInt8.self).baseAddress 225 | strm.avail_out = CUnsignedInt(bufferPtr.count) 226 | 227 | res = deflate(&strm, Z_SYNC_FLUSH) 228 | } 229 | 230 | let byteCount = buffer.count - Int(strm.avail_out) 231 | compressed.append(buffer, count: byteCount) 232 | } 233 | while res == Z_OK && strm.avail_out == 0 234 | 235 | } 236 | 237 | guard res == Z_OK && strm.avail_out > 0 238 | || (res == Z_BUF_ERROR && Int(strm.avail_out) == buffer.count) 239 | else { 240 | throw WSError(type: .compressionError, message: "Error on compressing", code: 0) 241 | } 242 | 243 | compressed.removeLast(4) 244 | return compressed 245 | } 246 | 247 | private func teardownDeflate() { 248 | if deflateInitialized, Z_OK == deflateEnd(&strm) { 249 | deflateInitialized = false 250 | } 251 | } 252 | 253 | deinit { 254 | teardownDeflate() 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /Sources/Engine/WSEngine.swift: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////////////////////////////////////////////////////// 2 | // 3 | // WSEngine.swift 4 | // Starscream 5 | // 6 | // Created by Dalton Cherry on 6/15/19 7 | // Copyright © 2019 Vluxe. All rights reserved. 8 | // 9 | // Licensed under the Apache License, Version 2.0 (the "License"); 10 | // you may not use this file except in compliance with the License. 11 | // You may obtain a copy of the License at 12 | // 13 | // http://www.apache.org/licenses/LICENSE-2.0 14 | // 15 | // Unless required by applicable law or agreed to in writing, software 16 | // distributed under the License is distributed on an "AS IS" BASIS, 17 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | // See the License for the specific language governing permissions and 19 | // limitations under the License. 20 | // 21 | ////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | import Foundation 24 | 25 | public class WSEngine: Engine, TransportEventClient, FramerEventClient, 26 | FrameCollectorDelegate, HTTPHandlerDelegate { 27 | private let transport: Transport 28 | private let framer: Framer 29 | private let httpHandler: HTTPHandler 30 | private let compressionHandler: CompressionHandler? 31 | private let certPinner: CertificatePinning? 32 | private let headerChecker: HeaderValidator 33 | private var request: URLRequest! 34 | 35 | private let frameHandler = FrameCollector() 36 | private var didUpgrade = false 37 | private var secKeyValue = "" 38 | private let writeQueue = DispatchQueue(label: "com.vluxe.starscream.writequeue") 39 | private let mutex = DispatchSemaphore(value: 1) 40 | private var canSend = false 41 | private var isConnecting = false 42 | 43 | weak var delegate: EngineDelegate? 44 | public var respondToPingWithPong: Bool = true 45 | 46 | public init(transport: Transport, 47 | certPinner: CertificatePinning? = nil, 48 | headerValidator: HeaderValidator = FoundationSecurity(), 49 | httpHandler: HTTPHandler = FoundationHTTPHandler(), 50 | framer: Framer = WSFramer(), 51 | compressionHandler: CompressionHandler? = nil) { 52 | self.transport = transport 53 | self.framer = framer 54 | self.httpHandler = httpHandler 55 | self.certPinner = certPinner 56 | self.headerChecker = headerValidator 57 | self.compressionHandler = compressionHandler 58 | framer.updateCompression(supports: compressionHandler != nil) 59 | frameHandler.delegate = self 60 | } 61 | 62 | public func register(delegate: EngineDelegate) { 63 | self.delegate = delegate 64 | } 65 | 66 | public func start(request: URLRequest) { 67 | mutex.wait() 68 | let isConnecting = self.isConnecting 69 | let isConnected = canSend 70 | mutex.signal() 71 | if isConnecting || isConnected { 72 | return 73 | } 74 | 75 | self.request = request 76 | transport.register(delegate: self) 77 | framer.register(delegate: self) 78 | httpHandler.register(delegate: self) 79 | frameHandler.delegate = self 80 | guard let url = request.url else { 81 | return 82 | } 83 | mutex.wait() 84 | self.isConnecting = true 85 | mutex.signal() 86 | transport.connect(url: url, timeout: request.timeoutInterval, certificatePinning: certPinner) 87 | } 88 | 89 | public func stop(closeCode: UInt16 = CloseCode.normal.rawValue) { 90 | let capacity = MemoryLayout.size 91 | var pointer = [UInt8](repeating: 0, count: capacity) 92 | writeUint16(&pointer, offset: 0, value: closeCode) 93 | let payload = Data(bytes: pointer, count: MemoryLayout.size) 94 | write(data: payload, opcode: .connectionClose, completion: { [weak self] in 95 | self?.reset() 96 | self?.forceStop() 97 | }) 98 | } 99 | 100 | public func forceStop() { 101 | mutex.wait() 102 | isConnecting = false 103 | mutex.signal() 104 | 105 | transport.disconnect() 106 | } 107 | 108 | public func write(string: String, completion: (() -> ())?) { 109 | let data = string.data(using: .utf8)! 110 | write(data: data, opcode: .textFrame, completion: completion) 111 | } 112 | 113 | public func write(data: Data, opcode: FrameOpCode, completion: (() -> ())?) { 114 | writeQueue.async { [weak self] in 115 | guard let s = self else { return } 116 | s.mutex.wait() 117 | let canWrite = s.canSend 118 | s.mutex.signal() 119 | if !canWrite { 120 | return 121 | } 122 | 123 | var isCompressed = false 124 | var sendData = data 125 | if let compressedData = s.compressionHandler?.compress(data: data) { 126 | sendData = compressedData 127 | isCompressed = true 128 | } 129 | 130 | let frameData = s.framer.createWriteFrame(opcode: opcode, payload: sendData, isCompressed: isCompressed) 131 | s.transport.write(data: frameData, completion: {_ in 132 | completion?() 133 | }) 134 | } 135 | } 136 | 137 | // MARK: - TransportEventClient 138 | 139 | public func connectionChanged(state: ConnectionState) { 140 | switch state { 141 | case .connected: 142 | secKeyValue = HTTPWSHeader.generateWebSocketKey() 143 | let wsReq = HTTPWSHeader.createUpgrade(request: request, supportsCompression: framer.supportsCompression(), secKeyValue: secKeyValue) 144 | let data = httpHandler.convert(request: wsReq) 145 | transport.write(data: data, completion: {_ in }) 146 | case .waiting: 147 | break 148 | case .failed(let error): 149 | handleError(error) 150 | case .viability(let isViable): 151 | broadcast(event: .viabilityChanged(isViable)) 152 | case .shouldReconnect(let status): 153 | broadcast(event: .reconnectSuggested(status)) 154 | case .receive(let data): 155 | if didUpgrade { 156 | framer.add(data: data) 157 | } else { 158 | let offset = httpHandler.parse(data: data) 159 | if offset > 0 { 160 | let extraData = data.subdata(in: offset.. Data? { 216 | return compressionHandler?.decompress(data: data, isFinal: isFinal) 217 | } 218 | 219 | public func didForm(event: FrameCollector.Event) { 220 | switch event { 221 | case .text(let string): 222 | broadcast(event: .text(string)) 223 | case .binary(let data): 224 | broadcast(event: .binary(data)) 225 | case .pong(let data): 226 | broadcast(event: .pong(data)) 227 | case .ping(let data): 228 | broadcast(event: .ping(data)) 229 | if respondToPingWithPong { 230 | write(data: data ?? Data(), opcode: .pong, completion: nil) 231 | } 232 | case .closed(let reason, let code): 233 | broadcast(event: .disconnected(reason, code)) 234 | stop(closeCode: code) 235 | case .error(let error): 236 | handleError(error) 237 | } 238 | } 239 | 240 | private func broadcast(event: WebSocketEvent) { 241 | delegate?.didReceive(event: event) 242 | } 243 | 244 | //This call can be coming from a lot of different queues/threads. 245 | //be aware of that when modifying shared variables 246 | private func handleError(_ error: Error?) { 247 | if let wsError = error as? WSError { 248 | stop(closeCode: wsError.code) 249 | } else { 250 | stop() 251 | } 252 | 253 | delegate?.didReceive(event: .error(error)) 254 | } 255 | 256 | private func reset() { 257 | mutex.wait() 258 | isConnecting = false 259 | canSend = false 260 | didUpgrade = false 261 | mutex.signal() 262 | } 263 | 264 | 265 | } 266 | --------------------------------------------------------------------------------