├── .swift-version ├── .gitignore ├── HotPotato ├── Potato.swift ├── PotatoVisit.swift ├── Queue.swift ├── LinkedList.swift ├── HotPotatoMessages.swift └── HotPotatoNetwork.swift ├── Core ├── BluepeerNavigationController.swift ├── BluepeerRowTableViewCell.swift ├── BluepeerBrowserViewController.swift └── BluepeerObject.swift ├── LICENSE ├── Bluepeer.podspec ├── README.md └── Assets └── Bluepeer.storyboard /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.xcworkspace 2 | Pods/ 3 | xcuserdata 4 | -------------------------------------------------------------------------------- /HotPotato/Potato.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Potato.swift 3 | // Created by Tim Carr on 2017-03-24. 4 | // Copyright © 2017 Tim Carr. All rights reserved. 5 | 6 | 7 | import Foundation 8 | import ObjectMapper 9 | 10 | let MaxPotatoVisitLength = 10 11 | 12 | public struct Potato { 13 | public var payload: Data? // Does NOT map 14 | public var visits: [PotatoVisit]? 15 | public var sentFromBackground = false 16 | } 17 | 18 | extension Potato : Mappable { 19 | public init?(map: Map) { 20 | if map.JSON["visits"] == nil { 21 | assert(false, "ERROR") 22 | return nil 23 | } 24 | } 25 | 26 | mutating public func mapping(map: Map) { 27 | visits <- map["visits"] 28 | sentFromBackground <- map["sentFromBackground"] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Core/BluepeerNavigationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluepeerNavigationController.swift 3 | // Pods 4 | // 5 | // Created by Tim Carr on 7/12/16. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | class BluepeerNavigationController: UINavigationController { 12 | 13 | override var preferredContentSize: CGSize { 14 | get { 15 | if let size = self.topViewController?.preferredContentSize { 16 | return size 17 | } else { 18 | return self.preferredContentSize 19 | } 20 | } 21 | set { 22 | self.preferredContentSize = newValue 23 | } 24 | } 25 | 26 | override func viewDidLoad() { 27 | super.viewDidLoad() 28 | 29 | // Do any additional setup after loading the view. 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /HotPotato/PotatoVisit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PotatoVisit.swift 3 | // Created by Tim Carr on 2017-03-24. 4 | // Copyright © 2017 Tim Carr. All rights reserved. 5 | 6 | 7 | import Foundation 8 | import ObjectMapper 9 | 10 | public struct PotatoVisit { 11 | var peerName: String? 12 | var visitNum: Int? 13 | var connectedPeers: [String]? 14 | } 15 | 16 | extension PotatoVisit : Mappable { 17 | public init?(map: Map) { 18 | if map.JSON["peerName"] == nil || map.JSON["visitNum"] == nil || map.JSON["connectedPeers"] == nil { 19 | assert(false, "ERROR") 20 | return nil 21 | } 22 | } 23 | 24 | mutating public func mapping(map: Map) { 25 | peerName <- map["peerName"] 26 | visitNum <- map["visitNum"] 27 | connectedPeers <- map["connectedPeers"] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /HotPotato/Queue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Queue.swift 3 | 4 | import Foundation 5 | 6 | public struct Queue { 7 | 8 | fileprivate var list = LinkedList() 9 | 10 | public var isEmpty: Bool { 11 | return list.isEmpty 12 | } 13 | 14 | public mutating func enqueue(_ element: T) { 15 | list.append(element) 16 | } 17 | 18 | public mutating func dequeue() -> T? { 19 | guard !list.isEmpty, let element = list.first else { return nil } 20 | 21 | let _ = list.remove(element) 22 | 23 | return element.value 24 | } 25 | 26 | public func peek() -> T? { 27 | return list.first?.value 28 | } 29 | } 30 | 31 | extension Queue: CustomStringConvertible { 32 | public var description: String { 33 | return list.description 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Tim Carr 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Core/BluepeerRowTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluepeerRowTableViewCell.swift 3 | // 4 | // Created by Tim Carr on 7/11/16. 5 | // Copyright © 2016 Tim Carr Photo. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | 10 | enum CellType { 11 | case loadingRow 12 | case normalRow 13 | } 14 | 15 | @objc open class BluepeerRowTableViewCell: UITableViewCell { 16 | 17 | var celltype: CellType = .normalRow 18 | var peer: BPPeer? 19 | 20 | @IBOutlet weak var mainLabel: UILabel! 21 | @IBOutlet weak var activityIndicator: UIActivityIndicatorView! 22 | 23 | override open func awakeFromNib() { 24 | super.awakeFromNib() 25 | // Initialization code 26 | } 27 | 28 | override open func setSelected(_ selected: Bool, animated: Bool) { 29 | super.setSelected(selected, animated: animated) 30 | 31 | // Configure the view for the selected state 32 | } 33 | 34 | open func updateDisplay() { 35 | switch celltype { 36 | case .loadingRow: 37 | self.mainLabel.text = "Searching..." 38 | self.selectionStyle = .none 39 | self.activityIndicator.startAnimating() 40 | 41 | case .normalRow: 42 | self.mainLabel.text = peer != nil ? peer!.displayName : "Unknown" 43 | self.selectionStyle = .default 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /HotPotato/LinkedList.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinkedList.swift 3 | // 4 | 5 | import Foundation 6 | 7 | public struct LinkedList: CustomStringConvertible { 8 | private var head: Node? 9 | private var tail: Node? 10 | 11 | public init() { } 12 | 13 | public var isEmpty: Bool { 14 | return head == nil 15 | } 16 | 17 | public var first: Node? { 18 | return head 19 | } 20 | 21 | public mutating func append(_ value: T) { 22 | let newNode = Node(value: value) 23 | if let tailNode = tail { 24 | newNode.previous = tailNode 25 | tailNode.next = newNode 26 | } else { 27 | head = newNode 28 | } 29 | tail = newNode 30 | } 31 | 32 | public mutating func remove(_ node: Node) -> T { 33 | let prev = node.previous 34 | let next = node.next 35 | 36 | if let prev = prev { 37 | prev.next = next 38 | } else { 39 | head = next 40 | } 41 | next?.previous = prev 42 | 43 | if next == nil { 44 | tail = prev 45 | } 46 | 47 | node.previous = nil 48 | node.next = nil 49 | 50 | return node.value 51 | } 52 | 53 | public var description: String { 54 | var text = "[" 55 | var node = head 56 | 57 | while node != nil { 58 | text += "\(node!.value)" 59 | node = node!.next 60 | if node != nil { text += ", " } 61 | } 62 | return text + "]" 63 | } 64 | } 65 | 66 | public class Node { 67 | public var value: T 68 | public var next: Node? 69 | public var previous: Node? 70 | 71 | public init(value: T) { 72 | self.value = value 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Bluepeer.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint filename.podspec' to ensure this is a 3 | # valid spec and remove all comments before submitting the spec. 4 | # 5 | # Any lines starting with a # are optional, but encouraged 6 | # 7 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 8 | # 9 | 10 | Pod::Spec.new do |s| 11 | s.name = "Bluepeer" 12 | s.version = "1.4.6" 13 | s.summary = "Provides adhoc Bluetooth and wifi networking at high-level" 14 | s.description = <<-DESC 15 | Provides P2P (adhoc) Bluetooth and wifi networking at high-level. Uses low-level frameworks like HHServices to have more control than Multipeer and NSNetService. 16 | DESC 17 | s.homepage = "https://github.com/xaphod/Bluepeer" 18 | s.license = 'MIT' 19 | s.author = { "Tim Carr" => "xaphod@gmail.com" } 20 | s.source = { :git => "https://github.com/xaphod/Bluepeer.git", :tag => s.version.to_s } 21 | 22 | s.platform = :ios, '11.0' 23 | s.requires_arc = true 24 | s.swift_version = '5.0' 25 | 26 | s.subspec 'Core' do |core| 27 | core.source_files = 'Core/*.{swift,m,h}' 28 | core.resource_bundles = { 29 | 'Bluepeer' => ['Assets/*.{lproj,storyboard}'] 30 | } 31 | core.dependency 'CocoaAsyncSocket', '>= 7.4.0' 32 | core.dependency 'HHServices', '>= 2.0' 33 | core.dependency 'xaphodObjCUtils', '>= 0.0.6' 34 | core.dependency 'DataCompression', '< 4.0.0' 35 | end 36 | 37 | s.subspec 'HotPotatoNetwork' do |hpn| 38 | hpn.source_files = 'HotPotato/*.{swift,m,h}' 39 | hpn.dependency 'Bluepeer/Core' 40 | hpn.dependency 'ObjectMapper', '~> 3.1' 41 | end 42 | 43 | #s.public_header_files = 'Pod/Classes/*.h' 44 | #s.xcconfig = {'OTHER_LDFLAGS' => '-ObjC -all_load'} 45 | #s.prefix_header_file = 'Pod/Classes/EOSFTPServer-Prefix.pch' 46 | #s.pod_target_xcconfig = {'SWIFT_INCLUDE_PATHS' => '$(SRCROOT)/Bluepeer/Pod/**'} 47 | #s.preserve_paths = 'Pod/Classes/module.modulemap' 48 | end 49 | -------------------------------------------------------------------------------- /HotPotato/HotPotatoMessages.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HotPotatoMessages.swift 3 | // 4 | // Created by Tim Carr on 2017-03-20. 5 | // Copyright © 2017 Tim Carr. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import ObjectMapper 10 | 11 | // Note: once something in the chain is StaticMappable, then all need to be StaticMappable 12 | 13 | open class HotPotatoMessage : StaticMappable { 14 | static open let protocolVersion = 1 15 | var type: String? // sub-struct name ie. "Start" 16 | 17 | open class func objectForMapping(map: Map) -> BaseMappable? { 18 | guard let version: Int = map["version"].value(), version == HotPotatoMessage.protocolVersion, let type: String = map["type"].value() else { 19 | assert(false, "MALFORMED MESSAGE OR MISMATCHED VERSION") 20 | return nil 21 | } 22 | 23 | switch type { 24 | case "StartHotPotatoMessage": 25 | return StartHotPotatoMessage() 26 | case "PotatoHotPotatoMessage": 27 | return PotatoHotPotatoMessage() 28 | case "BuildGraphHotPotatoMessage": 29 | return BuildGraphHotPotatoMessage() 30 | case "RecoverHotPotatoMessage": 31 | return RecoverHotPotatoMessage() 32 | case "PauseMeMessage": 33 | return PauseMeMessage() 34 | default: 35 | assert(false, "ERROR") 36 | return nil 37 | } 38 | } 39 | 40 | open func mapping(map: Map) { 41 | self.classNameAsString() >>> map["type"] 42 | HotPotatoMessage.protocolVersion >>> map["version"] 43 | type <- map["type"] 44 | } 45 | 46 | func classNameAsString() -> String { 47 | let parts = String(describing: self).components(separatedBy: ".") 48 | if parts.count == 0 { 49 | return parts[0] 50 | } else { 51 | return parts[1] 52 | } 53 | } 54 | 55 | init() { 56 | } 57 | } 58 | 59 | // means I want to start, I see N devices other than me, version of the data I have is FOO 60 | open class StartHotPotatoMessage : HotPotatoMessage { 61 | var remoteDevices: Int? 62 | var dataVersion: String? // ISO 8601 date format 63 | var ID: Int? // to map answers to requests 64 | var livePeerNames: [String:Int64]? 65 | var versionMismatchDetected = false 66 | 67 | override init() { 68 | super.init() 69 | } 70 | 71 | convenience init(remoteDevices: Int, dataVersion: String, ID: Int, livePeerNames: [String:Int64]?) { 72 | self.init() 73 | self.remoteDevices = remoteDevices 74 | self.dataVersion = dataVersion 75 | self.ID = ID 76 | self.livePeerNames = livePeerNames 77 | } 78 | 79 | override open func mapping(map: Map) { 80 | super.mapping(map: map) 81 | remoteDevices <- map["remoteDevices"] 82 | dataVersion <- map["dataVersion"] 83 | ID <- map["ID"] 84 | livePeerNames <- map["livePeerNames"] 85 | versionMismatchDetected <- map["versionMismatchDetected"] 86 | } 87 | } 88 | 89 | open class PotatoHotPotatoMessage : HotPotatoMessage { 90 | var potato: Potato? 91 | override init() { 92 | super.init() 93 | } 94 | convenience init(potato: Potato) { 95 | self.init() 96 | self.potato = potato 97 | } 98 | override open func mapping(map: Map) { 99 | super.mapping(map: map) 100 | potato <- map["potato"] 101 | } 102 | } 103 | 104 | open class BuildGraphHotPotatoMessage : HotPotatoMessage { 105 | var myConnectedPeers: [String]? 106 | var myState: HotPotatoNetwork.State? 107 | var livePeerNames: [String:Int64]? 108 | var ID: Int? // to map answers to requests 109 | override init() { 110 | super.init() 111 | } 112 | convenience init(myConnectedPeers: [String], myState: HotPotatoNetwork.State, livePeerNames: [String:Int64], ID: Int) { 113 | self.init() 114 | self.myConnectedPeers = myConnectedPeers 115 | self.myState = myState 116 | self.livePeerNames = livePeerNames 117 | self.ID = ID 118 | } 119 | override open func mapping(map: Map) { 120 | super.mapping(map: map) 121 | myConnectedPeers <- map["myConnectedPeers"] 122 | myState <- map["myState"] 123 | livePeerNames <- map["livePeerNames"] 124 | ID <- map["ID"] 125 | } 126 | } 127 | 128 | open class RecoverHotPotatoMessage : HotPotatoMessage { 129 | var ID: Int? // to map answers to requests 130 | var livePeerNames: [String:Int64]? 131 | override init() { 132 | super.init() 133 | } 134 | convenience init(ID: Int, livePeerNames: [String:Int64]?) { 135 | self.init() 136 | self.ID = ID 137 | self.livePeerNames = livePeerNames 138 | } 139 | 140 | override open func mapping(map: Map) { 141 | super.mapping(map: map) 142 | ID <- map["ID"] 143 | livePeerNames <- map["livePeerNames"] 144 | } 145 | } 146 | 147 | open class PauseMeMessage : HotPotatoMessage { 148 | var ID: Int? // to map answers to requests 149 | var isPause: Bool? // when false, is unpause 150 | var livePeerNames: [String:Int64]? // on a response to unpause, this contains the responder's live peer list -- in case it changed while the sender was backgrounded/out 151 | override init() { 152 | super.init() 153 | } 154 | convenience init(ID: Int, isPause: Bool, livePeerNames: [String:Int64]?) { 155 | self.init() 156 | self.ID = ID 157 | self.isPause = isPause 158 | self.livePeerNames = livePeerNames 159 | } 160 | 161 | override open func mapping(map: Map) { 162 | super.mapping(map: map) 163 | ID <- map["ID"] 164 | isPause <- map["isPause"] 165 | livePeerNames <- map["livePeerNames"] 166 | } 167 | } 168 | 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bluepeer 2 | 3 | Bluepeer provides adhoc Bluetooth and wifi connectivity for iOS 8+. Bluepeer is designed to be a MultiPeer replacement (Multipeer is Apple's MultipeerFramework, which lives inside GameKit). The APIs are as similar as possible in order to aid with migrations. Bluepeer has some key differences from Multipeer: 4 | 5 | - Can specify Bluetooth-only, instead of being forced to always use wifi with Multipeer (see next paragraph for why this is important) 6 | - Exposes a role-based system to more easily support client/server dichotomies (but you can set up roles however you like) 7 | - Uses sockets, TCP & unicast only (is not multicast like Multipeer is, and does not support UDP / `.unreliable` mode) 8 | - Is ideal for environments where devices are coming and going all the time, ie. you never call `stopAdvertising()` 9 | 10 | Bluepeer was written because Apple's Multipeer has a severe negative impact on wifi throughput / performance if you leave advertising on -- while advertising, wifi throughput is limited to about 100kB/sec instead of >1MB/sec on an iPad Air. That's why Apple advises that you call `stopAdvertising()` as soon as possible. Bluepeer avoids this by allowing advertising to only occur on the Bluetooth stack. 11 | 12 | Bluepeer is written in Swift, uses only publicly-accessible Apple APIs, and **is published in several apps on the Apple App Store** including [WiFi Booth](http://thewifibooth.com) and [BluePrint](https://thewifibooth.com/blueprint/). Bluepeer wouldn't have been possible without [HHServices](https://github.com/tolo/HHServices) by **tolo** -- many thanks Tobias! 13 | 14 | ## Please note this README / documentation is very out of date. Your best bet is to read thru BluepeerObject.swift and pay attention to the comments. I hope to publish a sample app soon that shows how to use it. 15 | 16 | As of iOS 10.0, it appears that Apple's Multipeer is broken unless wifi is on - ie. Bluetooth doesn't work. Bluepeer was updated for iOS 10 because it appears that iOS 10 blocks IPv4 advertising over Bluetooth (at least, while IPv6 is available). As a result, IPv6 addresses are now preferred for iOS versions >= 10. 17 | 18 | ### Requirements 19 | See the Podfile: Bluepeer requires `xaphodObjCUtils` from GitHub/Xaphod, and `HHServices` + `CocoaAsyncSocket` from the master cocoapods repo. 20 | 21 | ### Installation 22 | 23 | Easiest way is with Pods -- edit your Podfile and add 24 | 25 | ``` 26 | source 'https://github.com/xaphod/podspecs.git' 27 | pod 'Bluepeer' 28 | ``` 29 | ... and then `pod install` 30 | 31 | ### Usage 32 | 33 | Very similar to Multipeer. 34 | 35 | First, init a BluepeerObject. 36 | 37 | ```let bluepeer = BluepeerObject.init(serviceType: "serviceTypeStr", displayName: nil, queue: nil, serverPort: XaphodUtils.getFreeTCPPort(), overBluetoothOnly: btOnly, bluetoothBlock: nil)``` 38 | 39 | **serviceType** - Be careful with the serviceType, it needs to be a short string of *only* alphabetic characters (as it is the basis of a DNS TXT record). 40 | 41 | **displayName** - Optional. Specify a displayName if you wish other peers to see a specific name for this device; otherwise, the device's name will be used if you specify nil. 42 | 43 | **queue** - Optional. All delegate calls with be dispatched on this queue. If you leave it as nil, the main queue will be used. 44 | 45 | **serverPort** - Which TCP port will be used. Use getFreeTCPPort() as in the example above to retrieve a random port. The other devices do NOT need to know this, as it is auto-discovered via DNS TXT records. 46 | 47 | If you want to warn the user when Bluetooth is off, then set the bluetoothBlock like this: 48 | 49 | bluepeer.bluetoothBlock = { (state: BluetoothState) in 50 | if state == .PoweredOff { 51 | let alert = UIAlertController.init(title: "Bluetooth is Off", message: "This app can use both Bluetooth and WiFi. Bluetooth is almost always required. Please turn on Bluetooth now.", preferredStyle: .Alert) 52 | alert.addAction(UIAlertAction.init(title: "Cancel", style: .Cancel, handler: nil)) 53 | alert.addAction(UIAlertAction.init(title: "Open Settings", style: .Default, handler: { (_) in 54 | UIApplication.sharedApplication().openURL(NSURL.init(string: UIApplicationOpenSettingsURLString)!) 55 | })) 56 | self.presentViewController(alert, animated: true, completion: nil) 57 | } 58 | } 59 | 60 | At this point, someone needs to start advertising the service. Bluepeer has Server/Client roles built-in, here we'll be the server: 61 | 62 | bluepeer.sessionDelegate = self 63 | bluepeer.dataDelegate = self 64 | bluepeer.startAdvertising(.Server) 65 | 66 | ... so we need to implement the sessionDelegate: 67 | 68 | extension SomeClass: BluepeerSessionManagerDelegate { 69 | func peerConnectionRequest(peer: BPPeer, invitationHandler: (Bool) -> Void) { 70 | // ask the user if they want to accept the incoming connection. 71 | // call invitationHandler(true) or invitationHandler(false) accordingly 72 | } 73 | 74 | func peerDidConnect(peerRole: RoleType, peer: BPPeer) { 75 | // when a peer has successfully connected 76 | } 77 | 78 | func peerDidDisconnect(peerRole: RoleType, peer: BPPeer) { 79 | // when a peer has disconnected (intentionally, or lost connection) 80 | } 81 | } 82 | 83 | ... and the dataDelegate: 84 | 85 | extension SomeClass: BluepeerDataDelegate { 86 | func didReceiveData(data: NSData, fromPeer peerID: BPPeer) { 87 | // received data from BPPeer fromPeer ! do something with it 88 | } 89 | } 90 | 91 | On the side that needs to initiate the connection, you have a couple of options. You can do it programmatically, or you can present a browser to the user like this: 92 | 93 | let browserViewController = bluepeer.getBrowser { (success) in 94 | if (success) { 95 | self.performSegueWithIdentifier("segueToSomewhere", sender: self) 96 | } 97 | 98 | if (UIDevice.currentDevice().userInterfaceIdiom == .Phone) { 99 | presentViewController(browser, animated: true, completion: nil) 100 | } else { 101 | browser.modalPresentationStyle = .Popover 102 | if let popBrowser: UIPopoverPresentationController = browser.popoverPresentationController { 103 | popBrowser.sourceView = self.view 104 | popBrowser.sourceRect = self.someButton.frame 105 | presentViewController(browser, animated: true, completion: nil) 106 | } 107 | } 108 | 109 | To send data, you can use either toRole or toPeers depending on what you want: 110 | 111 | bluepeer.sendData([imageData], toRole: .Server) 112 | ... will send to all connected BPPeer with the role Server 113 | 114 | bluepeer.sendData([imageData], toPeers: [peer1, peer2]) 115 | ... will send to peer1 and peer2. 116 | 117 | Because Apple does not allow adhoc networking like this to function while the app is in the background, BluepeerObjects will automatically disconnect themselves when your app resigns active. 118 | 119 | 120 | -------------------------------------------------------------------------------- /Core/BluepeerBrowserViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluepeerBrowserViewController.swift 3 | // 4 | // Created by Tim Carr on 7/11/16. 5 | // Copyright © 2016 Tim Carr Photo. All rights reserved. 6 | // 7 | 8 | import UIKit 9 | import xaphodObjCUtils 10 | 11 | @objc open class BluepeerBrowserViewController: UITableViewController { 12 | 13 | var bluepeerObject: BluepeerObject? 14 | var bluepeerSuperAdminDelegate: BluepeerMembershipAdminDelegate? 15 | var bluepeerSuperRosterDelegate: BluepeerMembershipRosterDelegate? 16 | open var browserCompletionBlock: ((Bool) -> ())? 17 | var peers: [BPPeer] = [] 18 | var progressView: XaphodProgressView? 19 | var lastTimerStarted: Date? 20 | var timer: Timer? 21 | 22 | @IBOutlet weak var cancelButton: UIBarButtonItem! 23 | 24 | override open var preferredContentSize: CGSize { 25 | get { 26 | return CGSize(width: 320, height: 280) 27 | } 28 | set { 29 | self.preferredContentSize = newValue 30 | } 31 | } 32 | 33 | override open func viewDidLoad() { 34 | super.viewDidLoad() 35 | if #available(iOS 13.0, *) { 36 | self.isModalInPresentation = true 37 | } 38 | self.setNeedsStatusBarAppearanceUpdate() 39 | guard let bo = bluepeerObject else { 40 | assert(false, "ERROR: set bluepeerObject before loading view") 41 | return 42 | } 43 | // this should be in viewDidLoad. 44 | self.bluepeerSuperAdminDelegate = bo.membershipAdminDelegate 45 | self.bluepeerSuperRosterDelegate = bo.membershipRosterDelegate 46 | bo.membershipRosterDelegate = self 47 | bo.membershipAdminDelegate = self 48 | bo.startBrowsing() 49 | } 50 | 51 | override open func viewWillAppear(_ animated: Bool) { 52 | super.viewWillAppear(animated) 53 | } 54 | 55 | override open func viewWillDisappear(_ animated: Bool) { 56 | super.viewWillDisappear(animated) 57 | self.bluepeerObject?.stopBrowsing() 58 | self.bluepeerObject?.membershipAdminDelegate = self.bluepeerSuperAdminDelegate 59 | self.bluepeerObject?.membershipRosterDelegate = self.bluepeerSuperRosterDelegate 60 | self.bluepeerObject = nil 61 | self.lastTimerStarted = nil 62 | self.timer?.invalidate() 63 | self.timer = nil 64 | } 65 | 66 | override open var prefersStatusBarHidden: Bool { 67 | return false 68 | } 69 | 70 | override open var preferredStatusBarStyle: UIStatusBarStyle { 71 | return .default 72 | } 73 | 74 | // MARK: - Table view data source 75 | 76 | override open func numberOfSections(in tableView: UITableView) -> Int { 77 | return 1 78 | } 79 | 80 | override open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 81 | return max(self.peers.count, 1) 82 | } 83 | 84 | override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 85 | var cell: BluepeerRowTableViewCell 86 | if self.peers.count == 0 { 87 | // loading row 88 | cell = tableView.dequeueReusableCell(withIdentifier: "loadingRow", for: indexPath) as! BluepeerRowTableViewCell 89 | cell.celltype = .loadingRow 90 | } else { 91 | cell = tableView.dequeueReusableCell(withIdentifier: "peerRow", for: indexPath) as! BluepeerRowTableViewCell 92 | cell.celltype = .normalRow 93 | cell.peer = self.peers[(indexPath as NSIndexPath).row] 94 | } 95 | cell.updateDisplay() 96 | 97 | return cell 98 | } 99 | 100 | override open func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { 101 | if self.peers.count == 0 { 102 | return nil 103 | } else { 104 | return indexPath 105 | } 106 | } 107 | 108 | override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 109 | self.progressView = XaphodProgressView.init(view: self.view) 110 | self.view.addSubview(self.progressView!) 111 | self.progressView!.text = "" 112 | self.progressView!.show(withAnimation: true) 113 | self.lastTimerStarted = Date.init() 114 | self.timer = Timer.scheduledTimer(timeInterval: 60.0, target: self, selector: #selector(timerFired), userInfo: nil, repeats: false) 115 | 116 | let peer = self.peers[(indexPath as NSIndexPath).row] 117 | let _ = peer.connect?() 118 | } 119 | 120 | @IBAction func cancelPressed(_ sender: AnyObject) { 121 | self.bluepeerObject?.disconnectSession() 122 | self.dismiss(animated: true, completion: { 123 | self.browserCompletionBlock?(false) 124 | }) 125 | } 126 | 127 | @objc func timerFired(_ timer: Timer) { 128 | NSLog("Timer fired.") 129 | if let _ = self.lastTimerStarted { 130 | if (self.progressView != nil) { 131 | self.progressView?.dismiss(withAnimation: true) 132 | self.progressView = nil 133 | } 134 | } 135 | } 136 | } 137 | 138 | extension BluepeerBrowserViewController: BluepeerMembershipRosterDelegate { 139 | public func bluepeer(_ bluepeerObject: BluepeerObject, peerDidConnect peerRole: RoleType, peer: BPPeer) { 140 | DispatchQueue.main.async(execute: { 141 | NSLog("BluepeerBrowserVC: connected, dismissing.") 142 | self.progressView?.dismiss(withAnimation: false) 143 | self.progressView = nil 144 | self.lastTimerStarted = nil 145 | self.timer?.invalidate() 146 | self.timer = nil 147 | 148 | self.dismiss(animated: true, completion: { 149 | self.browserCompletionBlock?(true) 150 | }) 151 | }) 152 | } 153 | 154 | public func bluepeer(_ bluepeerObject: BluepeerObject, peerConnectionAttemptFailed peerRole: RoleType, peer: BPPeer?, isAuthRejection: Bool, canConnectNow: Bool) { 155 | DispatchQueue.main.async(execute: { 156 | self.progressView?.dismiss(withAnimation: false) 157 | self.progressView = nil 158 | self.lastTimerStarted = nil 159 | self.timer?.invalidate() 160 | self.timer = nil 161 | 162 | self.dismiss(animated: true, completion: { 163 | self.browserCompletionBlock?(false) 164 | }) 165 | }) 166 | } 167 | } 168 | 169 | extension BluepeerBrowserViewController: BluepeerMembershipAdminDelegate { 170 | public func bluepeer(_ bluepeerObject: BluepeerObject, browserFoundPeer role: RoleType, peer: BPPeer) { 171 | DispatchQueue.main.async(execute: { 172 | if !self.peers.contains(peer) { 173 | self.peers.append(peer) 174 | self.tableView.reloadData() 175 | } 176 | }) 177 | } 178 | 179 | public func bluepeer(_ bluepeerObject: BluepeerObject, browserLostPeer role: RoleType, peer: BPPeer) { 180 | DispatchQueue.main.async(execute: { 181 | self.progressView?.dismiss(withAnimation: false) 182 | self.progressView = nil 183 | self.lastTimerStarted = nil 184 | self.timer?.invalidate() 185 | self.timer = nil 186 | if let index = self.peers.firstIndex(where: {$0 == peer}) { 187 | self.peers.remove(at: index) 188 | self.tableView.reloadData() 189 | } 190 | }) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Assets/Bluepeer.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 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /HotPotato/HotPotatoNetwork.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HotPotatoNetwork.swift 3 | // 4 | // Created by Tim Carr on 2017-03-07. 5 | // Copyright © 2017 Tim Carr. All rights reserved. 6 | 7 | import Foundation 8 | import xaphodObjCUtils 9 | import ObjectMapper 10 | 11 | public protocol HandlesHotPotatoMessages { 12 | var isActiveAsHandler: Bool { get } 13 | func handleHotPotatoMessage(message: HotPotatoMessage, peer: BPPeer, HPN: HotPotatoNetwork) 14 | } 15 | 16 | public protocol HandlesPotato { 17 | func youStartThePotato() -> Data? // one of the peers will be asked to start the potato-passing; this function provides the data 18 | func youHaveThePotato(potato: Potato, finishBlock: @escaping (_ potato: Potato)->Void) 19 | } 20 | 21 | public protocol HandlesStateChanges { 22 | func didChangeState(from: HotPotatoNetwork.State, to: HotPotatoNetwork.State) 23 | func showError(type: HotPotatoNetwork.HPNError, title: String, message: String) 24 | func thesePeersAreMissing(peerNames: [String], dropBlock: @escaping ()->Void, keepWaitingBlock: @escaping ()->Void) 25 | func didChangeRoster() // call activePeerNamesIncludingSelf() to get roster 26 | } 27 | 28 | open class HotPotatoNetwork: CustomStringConvertible { 29 | 30 | open var bluepeer: BluepeerObject? 31 | open var logDelegate: BluepeerLoggingDelegate? 32 | open var messageHandlers = [String:HandlesHotPotatoMessages]() // dict of message TYPE -> HotPotatoMessageHandler (one per message.type) 33 | open var potatoDelegate: HandlesPotato? 34 | open var stateDelegate: HandlesStateChanges? 35 | 36 | public enum State: Int { 37 | case buildup = 1 38 | case live 39 | case disconnect 40 | case finished // done, cannot start again 41 | } 42 | public enum HPNError: Error { 43 | case versionMismatch 44 | case startClientCountMismatch 45 | case noCustomData 46 | } 47 | 48 | fileprivate var messageReplyQueue = [String:Queue<(HotPotatoMessage)->Void>]() // dict of message TYPE -> an queue of blocks that take a HotPotatoMessage. These are put here when people SEND data, as replyHandlers - they run max once. 49 | fileprivate var deviceIdentifier: String = UIDevice.current.name 50 | open var livePeerNames = [String:Int64]() // name->customdata[id]. peers that were included when Start button was pressed. Includes self, unlike bluepeer.peers which does not! 51 | fileprivate var livePeerStatus = [String:Bool]() // name->true meaning is active (not paused) 52 | fileprivate var potatoLastPassedDates = [String:Date]() 53 | fileprivate var potato: Potato? { 54 | didSet { 55 | potatoLastSeen = Date.init() 56 | } 57 | } 58 | fileprivate var potatoLastSeen: Date = Date.init(timeIntervalSince1970: 0) 59 | fileprivate let payloadHeader = "[:!Payload Header Start!:]".data(using: .utf8)! 60 | fileprivate var potatoTimer: Timer? 61 | fileprivate var potatoTimerSeconds: TimeInterval! 62 | fileprivate var networkName: String! 63 | fileprivate var dataVersion: String! // ISO 8601 date 64 | 65 | fileprivate var state: State = .buildup { 66 | didSet { 67 | if oldValue == state { 68 | return 69 | } 70 | 71 | if oldValue == .finished && state != .finished { 72 | assert(false, "ERROR: must not transition from finished to something else") 73 | return 74 | } 75 | 76 | if oldValue == .buildup && state == .live { 77 | self.logDelegate?.logString("*** BUILDUP -> LIVE ***") 78 | } else if oldValue == .live && state == .disconnect { 79 | self.logDelegate?.logString("*** LIVE -> DISCONNECT ***") 80 | } else if oldValue == .disconnect && state == .live { 81 | self.logDelegate?.logString("*** DISCONNECT -> LIVE ***") 82 | } else if state == .finished { 83 | self.logDelegate?.logString("*** STATE = FINISHED ***") 84 | } else { 85 | assert(false, "ERROR: invalid state transition") 86 | } 87 | 88 | self.stateDelegate?.didChangeState(from: oldValue, to: state) 89 | } 90 | } 91 | 92 | fileprivate var _messageID: Int = Int(arc4random_uniform(10000)) // start a random number so that IDs don't collide from different originators 93 | fileprivate var messageID: Int { 94 | get { 95 | return _messageID 96 | } 97 | set(newID) { 98 | if newID == Int.max-1 { 99 | _messageID = 0 100 | } else { 101 | _messageID = newID 102 | } 103 | } 104 | } 105 | 106 | // START message 107 | fileprivate var livePeersOnStart = 0 108 | fileprivate var startRepliesReceived = 0 109 | fileprivate var startHotPotatoMessageID = 0 110 | 111 | // BUILDGRAPH message 112 | fileprivate var buildGraphHotPotatoMessageID = 0 113 | fileprivate var missingPeersFromLiveList: [String]? 114 | 115 | // RECEIVING A POTATO -> PAYLOAD 116 | fileprivate var pendingPotatoHotPotatoMessage: PotatoHotPotatoMessage? // if not nil, then the next received data is the potato's data payload 117 | 118 | // TELLING PEERS TO PAUSE ME WHEN I GO TO BACKGROUND. I send a PauseMeMessage, wait for responses from connected peers, then end bgTask 119 | fileprivate var backgroundTask = UIBackgroundTaskInvalid // if not UIBackgroundTaskInvalid, then we backgrounded a live session 120 | fileprivate var pauseMeMessageID = 0 121 | fileprivate var pauseMeMessageNumExpectedResponses = 0 122 | fileprivate var pauseMeMessageResponsesSeen = 0 123 | fileprivate var onConnectUnpauseBlock: (()->Bool)? 124 | fileprivate var didSeeWillEnterForeground = false 125 | 126 | 127 | // all peers in this network must use the same name and version to connect and start. timeout is the amount of time the potato must be seen within, until it is considered a disconnect 128 | required public init(networkName: String, dataVersion: Date, timeout: TimeInterval? = 15.0) { 129 | self.networkName = networkName 130 | self.dataVersion = DateFormatter.ISO8601DateFormatter().string(from: dataVersion) 131 | self.potatoTimerSeconds = timeout 132 | NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: Notification.Name.UIApplicationDidEnterBackground, object: nil) 133 | NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: Notification.Name.UIApplicationWillEnterForeground, object: nil) 134 | NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: Notification.Name.UIApplicationDidBecomeActive, object: nil) 135 | } 136 | 137 | deinit { 138 | NotificationCenter.default.removeObserver(self) 139 | } 140 | 141 | // removes me from the network, doesn't stop others from continuing in the network 142 | open func stop() { 143 | // TODO: implement new IAmLeaving Message so the others don't have to wait 144 | self.logDelegate?.logString("HPN: STOP() CALLED, DISCONNECTING SESSION") 145 | self.potatoTimer?.invalidate() 146 | self.potatoTimer = nil 147 | self.state = .finished 148 | self.bluepeer!.stopAdvertising() 149 | self.bluepeer!.stopBrowsing() 150 | self.bluepeer!.dataDelegate = nil 151 | self.bluepeer!.membershipRosterDelegate = nil 152 | self.bluepeer!.membershipAdminDelegate = nil 153 | self.bluepeer!.logDelegate = nil 154 | self.bluepeer!.disconnectSession() 155 | } 156 | 157 | open func startLookingForPotatoPeers(interfaces: BluepeerInterfaces) { 158 | var bluepeerServiceName = self.networkName! 159 | if bluepeerServiceName.count > 15 { 160 | bluepeerServiceName = bluepeerServiceName.substring(to: bluepeerServiceName.index(bluepeerServiceName.startIndex, offsetBy:14)) 161 | } 162 | self.logDelegate?.logString("HPN: startConnecting, Bluepeer service name: \(bluepeerServiceName), device: \(self.deviceIdentifier) / \(self.deviceIdentifier.hashValue)") 163 | self.bluepeer = BluepeerObject.init(serviceType: bluepeerServiceName, displayName: deviceIdentifier, queue: nil, serverPort: XaphodUtils.getFreeTCPPort(), interfaces: interfaces, bluetoothBlock: nil)! 164 | if let logDel = self.logDelegate { 165 | self.bluepeer?.logDelegate = logDel 166 | } 167 | 168 | self.bluepeer!.dataDelegate = self 169 | self.bluepeer!.membershipAdminDelegate = self 170 | self.bluepeer!.membershipRosterDelegate = self 171 | self.bluepeer!.startBrowsing() 172 | self.bluepeer!.startAdvertising(.any, customData: ["id":String(self.deviceIdentifier.hashValue)]) 173 | } 174 | 175 | open var description: String { 176 | return self.bluepeer?.description ?? "Bluepeer not initialized" 177 | } 178 | 179 | // per Apple: "Your implementation of this method has approximately five seconds to perform any tasks and return. If you need additional time to perform any final tasks, you can request additional execution time from the system by calling beginBackgroundTask(expirationHandler:)" 180 | @objc fileprivate func didEnterBackground() { 181 | guard self.state != .buildup, self.state != .finished, let bluepeer = self.bluepeer else { return } 182 | 183 | self.logDelegate?.logString("HPN: didEnterBackground() with live session, sending PauseMeMessage") 184 | if let timer = self.potatoTimer { 185 | timer.invalidate() 186 | self.potatoTimer = nil 187 | } 188 | 189 | guard bluepeer.connectedPeers().count > 0 else { 190 | self.logDelegate?.logString("HPN: didEnterBackground() with live session but NO CONNECTED PEERS, no-op") 191 | self.backgroundTask = 10 // Must be anything != UIBackgroundTaskInvalid 192 | return 193 | } 194 | self.backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "HotPotatoNetwork") { 195 | self.logDelegate?.logString("HPN WARNING: backgroundTask expirationHandler() called! We're not fast enough..?") 196 | assert(false, "ERROR") 197 | } 198 | 199 | messageID += 1 200 | pauseMeMessageID = messageID 201 | pauseMeMessageResponsesSeen = 0 202 | pauseMeMessageNumExpectedResponses = bluepeer.connectedPeers().count 203 | let message = PauseMeMessage.init(ID: pauseMeMessageID, isPause: true, livePeerNames: nil) 204 | self.sendHotPotatoMessage(message: message, replyBlock: nil) 205 | } 206 | 207 | @objc fileprivate func willEnterForeground() { 208 | guard self.backgroundTask != UIBackgroundTaskInvalid, self.state != .buildup, state != .finished, let _ = self.bluepeer else { return } 209 | self.backgroundTask = UIBackgroundTaskInvalid 210 | self.didSeeWillEnterForeground = true 211 | } 212 | 213 | @objc fileprivate func didBecomeActive() { 214 | guard self.didSeeWillEnterForeground == true else { return } 215 | self.didSeeWillEnterForeground = false 216 | self.logDelegate?.logString("HPN: didBecomeActive() with backgrounded live session, recovery expected. Restarting potato timer, and sending UNPAUSE PauseMeMessage...") 217 | self.restartPotatoTimer() 218 | self.pauseMeMessageNumExpectedResponses = 0 219 | self.pauseMeMessageResponsesSeen = 0 220 | 221 | messageID += 1 222 | if self.bluepeer!.connectedPeers().count >= 1 { 223 | self.logDelegate?.logString("HPN: unpausing") 224 | self.pauseMeMessageID = messageID 225 | self.sendHotPotatoMessage(message: PauseMeMessage.init(ID: messageID, isPause: false, livePeerNames: nil), replyBlock: nil) 226 | } else { 227 | self.logDelegate?.logString("HPN: can't unpause yet, waiting for a connection...") 228 | self.onConnectUnpauseBlock = { 229 | self.logDelegate?.logString("HPN: >=1 connection back, unpausing (PauseMeNow message)") 230 | self.pauseMeMessageID = self.messageID 231 | self.sendHotPotatoMessage(message: PauseMeMessage.init(ID: self.messageID, isPause: false, livePeerNames: nil), replyBlock: nil) 232 | return true 233 | } 234 | } 235 | } 236 | 237 | open func activePeerNamesIncludingSelf() -> [String] { 238 | let peers = self.bluepeer!.connectedPeers().filter { 239 | if let status = self.livePeerStatus[$0.displayName] { 240 | if let _ = self.livePeerNames[$0.displayName] { 241 | return status == true 242 | } 243 | } 244 | return false 245 | } 246 | var retval = peers.map({ return $0.displayName }) 247 | retval.append(self.bluepeer!.displayNameSanitized) 248 | return retval 249 | } 250 | 251 | // MARK: 252 | // MARK: SENDING MESSAGES & CONNECTIONS 253 | // MARK: 254 | 255 | open func sendHotPotatoMessage(message: HotPotatoMessage, replyBlock: ((HotPotatoMessage)->Void)?) { 256 | guard let str: String = message.toJSONString(prettyPrint: false), let data = str.data(using: .utf8) else { 257 | assert(false, "ERROR can't make data out of HotPotatoMessage") 258 | return 259 | } 260 | do { 261 | self.prepReplyQueueWith(replyBlock: replyBlock, message: message) 262 | try self.bluepeer!.sendData([data], toRole: .any) 263 | if let message = message as? PotatoHotPotatoMessage, let payload = message.potato?.payload { 264 | var headeredPayload = payloadHeader 265 | headeredPayload.append(payload) 266 | try self.bluepeer!.sendData([headeredPayload], toRole: .any) 267 | } 268 | } catch { 269 | assert(false, "ERROR got error on sendData: \(error)") 270 | } 271 | } 272 | 273 | open func sendHotPotatoMessage(message: HotPotatoMessage, toPeer: BPPeer, replyBlock: ((HotPotatoMessage)->Void)?) { 274 | guard let str: String = message.toJSONString(prettyPrint: false), let data = str.data(using: .utf8) else { 275 | assert(false, "ERROR can't make data out of HotPotatoMessage") 276 | return 277 | } 278 | do { 279 | self.prepReplyQueueWith(replyBlock: replyBlock, message: message) 280 | try self.bluepeer!.sendData([data], toPeers: [toPeer]) 281 | if let message = message as? PotatoHotPotatoMessage, let payload = message.potato?.payload { 282 | var headeredPayload = payloadHeader 283 | headeredPayload.append(payload) 284 | try self.bluepeer!.sendData([headeredPayload], toPeers: [toPeer]) 285 | } 286 | } catch { 287 | assert(false, "ERROR got error on sendData: \(error)") 288 | } 289 | } 290 | 291 | fileprivate func prepReplyQueueWith(replyBlock: ((HotPotatoMessage)->Void)?, message: HotPotatoMessage) { 292 | if let replyBlock = replyBlock { 293 | let key = message.classNameAsString() 294 | if self.messageReplyQueue[key] == nil { 295 | self.messageReplyQueue[key] = Queue<(HotPotatoMessage)->Void>() 296 | } 297 | 298 | self.messageReplyQueue[key]!.enqueue(replyBlock) 299 | } 300 | } 301 | 302 | fileprivate func connectionAllowedFrom(peer: BPPeer) -> Bool { 303 | if self.livePeerNames.count > 0 { 304 | if let _ = self.livePeerNames[peer.displayName] { 305 | self.logDelegate?.logString(("HPN: livePeerNames contains this peer, allowing connection")) 306 | return true 307 | } else { 308 | self.logDelegate?.logString(("HPN: livePeerNames is non-empty and DOES NOT contain \(peer.displayName), NOT ALLOWING CONNECTION")) 309 | return false 310 | } 311 | } else { 312 | self.logDelegate?.logString(("HPN: no livePeerNames so we're in buildup phase - allowing all connections")) 313 | return true 314 | } 315 | } 316 | 317 | fileprivate func connectToPeer(_ peer: BPPeer) { 318 | if self.connectionAllowedFrom(peer: peer) == false { 319 | return 320 | } 321 | 322 | guard let remoteID = peer.customData["id"] as? String, let remoteIDInt = Int64(remoteID) else { 323 | self.logDelegate?.logString("HPN: ERROR, remote ID missing/invalid - \(String(describing: peer.customData["id"]))") 324 | return 325 | } 326 | if remoteIDInt > Int64(self.deviceIdentifier.hashValue) { 327 | self.logDelegate?.logString("HPN: remote ID(\(remoteIDInt)) bigger than mine(\(self.deviceIdentifier.hashValue)), initiating connection...") 328 | let _ = peer.connect!() 329 | } else { 330 | self.logDelegate?.logString("HPN: remote ID(\(remoteIDInt)) smaller than mine(\(self.deviceIdentifier.hashValue)), no-op.") 331 | } 332 | } 333 | 334 | // MARK: 335 | // MARK: POTATO 336 | // MARK: 337 | 338 | 339 | fileprivate func startPotatoNow() { 340 | assert(livePeerNames.count > 0, "ERROR") 341 | guard let payload = self.potatoDelegate?.youStartThePotato() else { 342 | assert(false, "ERROR set potatoDelegate first, and make sure it can always give me a copy of the payload Data") 343 | return 344 | } 345 | for peerName in livePeerNames { 346 | potatoLastPassedDates[peerName.0] = Date.init(timeIntervalSince1970: 0) 347 | } 348 | potatoLastPassedDates.removeValue(forKey: self.bluepeer!.displayNameSanitized) 349 | 350 | // highest hash of peernames wins 351 | var winner: (String, Int64) = livePeerNames.reduce(("",Int64.min)) { (result, element) -> (String,Int64) in 352 | guard let status = self.livePeerStatus[element.key] else { 353 | assert(false, "no status ERROR") 354 | return result 355 | } 356 | if status == false { 357 | // element is a paused peer, can't win 358 | return result 359 | } 360 | return element.value >= result.1 ? element : result 361 | } 362 | if winner.0 == "" { 363 | self.logDelegate?.logString("startPotatoNow: no one to start the potato so i'll do it") 364 | winner.0 = self.bluepeer!.displayNameSanitized 365 | } 366 | 367 | if self.bluepeer!.displayNameSanitized == winner.0 { 368 | self.logDelegate?.logString("startPotatoNow: I'M THE WINNER") 369 | let firstVisit = self.generatePotatoVisit() 370 | self.potato = Potato.init(payload: payload, visits: [firstVisit], sentFromBackground: false) 371 | self.passPotato() 372 | } else { 373 | self.logDelegate?.logString("startPotatoNow: I didn't win, \(winner) did") 374 | self.restartPotatoTimer() 375 | } 376 | } 377 | 378 | fileprivate func passPotato() { 379 | if state == .disconnect || state == .finished { 380 | self.logDelegate?.logString("NOT passing potato as state=disconnect/finished") 381 | return 382 | } 383 | 384 | let oldestPeer: (String,Date) = self.potatoLastPassedDates.reduce(("", Date.distantFuture)) { (result, element) -> (String, Date) in 385 | let peers = self.bluepeer!.peers.filter({ $0.displayName == element.0 }) 386 | guard peers.count == 1 else { 387 | return result // this can happen during reconnect, because dupe peer is being removed just at the moment this gets hit 388 | } 389 | let peer = peers.first! 390 | guard peer.state == .authenticated else { 391 | self.logDelegate?.logString("passPotato: \(peer.displayName) not connected, not passing to them...") 392 | return result 393 | } 394 | guard let status = livePeerStatus[element.0], status == true else { 395 | self.logDelegate?.logString("passPotato: \(peer.displayName) is paused, not passing to them...") 396 | return result 397 | } 398 | return result.1 < element.1 ? result : element 399 | } 400 | 401 | if oldestPeer.0 == "" { 402 | self.logDelegate?.logString("potatoPassBlock: FOUND NO PEER TO PASS TO, EATING POTATO AGAIN") 403 | self.potatoDelegate?.youHaveThePotato(potato: self.potato!, finishBlock: { (potato) in 404 | self.potato = potato 405 | self.passPotato() 406 | }) 407 | return 408 | } 409 | self.logDelegate?.logString("potatoPassBlock: passing to \(oldestPeer.0)") 410 | 411 | // update potato meta 412 | self.potatoLastPassedDates[oldestPeer.0] = Date.init() 413 | var visits: [PotatoVisit] = Array(potato!.visits!.suffix(MaxPotatoVisitLength-1)) 414 | visits.append(self.generatePotatoVisit()) 415 | potato!.visits = visits 416 | potato!.sentFromBackground = UIApplication.shared.applicationState == .background 417 | 418 | let potatoHotPotatoMessage = PotatoHotPotatoMessage.init(potato: potato!) 419 | 420 | self.sendHotPotatoMessage(message: potatoHotPotatoMessage, toPeer: self.bluepeer!.peers.filter({ $0.displayName == oldestPeer.0 }).first!, replyBlock: nil) 421 | self.restartPotatoTimer() 422 | } 423 | 424 | // TODO: remove potatoVisits if they are not in use 425 | fileprivate func generatePotatoVisit() -> PotatoVisit { 426 | var visitNum = 1 427 | if let potato = self.potato { // should be true every time except the first one 428 | visitNum = potato.visits!.last!.visitNum! + 1 429 | } 430 | let connectedPeers = self.bluepeer!.connectedPeers().map({ $0.displayName }) 431 | let visit = PotatoVisit.init(peerName: self.bluepeer!.displayNameSanitized, visitNum: visitNum, connectedPeers: connectedPeers) 432 | self.logDelegate?.logString("generatePotatoVisit: generated \(visit)") 433 | return visit 434 | } 435 | 436 | fileprivate func restartPotatoTimer() { 437 | guard self.backgroundTask == UIBackgroundTaskInvalid else { return } // don't reschedule the timer when we're in the background 438 | 439 | if let timer = self.potatoTimer { 440 | timer.invalidate() 441 | self.potatoTimer = nil 442 | } 443 | self.potatoTimer = Timer.scheduledTimer(timeInterval: potatoTimerSeconds, target: self, selector: #selector(potatoTimerFired(timer:)), userInfo: nil, repeats: false) 444 | } 445 | 446 | @objc fileprivate func potatoTimerFired(timer: Timer) { 447 | guard self.state != .finished else { 448 | timer.invalidate() 449 | self.potatoTimer = nil 450 | return 451 | } 452 | 453 | let timeSincePotatoLastSeen = abs(self.potatoLastSeen.timeIntervalSinceNow) 454 | self.logDelegate?.logString("Potato Timer Fired. Last seen: \(timeSincePotatoLastSeen)") 455 | 456 | if timeSincePotatoLastSeen > potatoTimerSeconds { 457 | // state: disconnect 458 | self.logDelegate?.logString("POTATO TIMER SETS STATE=DISCONNECT, sending BuildGraph messages") 459 | self.state = .disconnect 460 | DispatchQueue.main.asyncAfter(deadline: .now() + potatoTimerSeconds, execute: { 461 | // delay so that all devices can get to disconnect state before they respond to our BuildGraph message! 462 | self.sendBuildGraphHotPotatoMessage() 463 | }) 464 | } 465 | } 466 | 467 | fileprivate func handlePotatoHotPotatoMessage(_ potatoHotPotatoMessage: PotatoHotPotatoMessage, peer: BPPeer) { 468 | self.potato = potatoHotPotatoMessage.potato! 469 | self.logDelegate?.logString("HPN: got potato from \(peer.displayName).") 470 | assert(self.potatoLastPassedDates[peer.displayName] != nil, "ERROR") 471 | self.potatoLastPassedDates[peer.displayName] = Date.init() 472 | 473 | if self.potato!.sentFromBackground == false { 474 | let oldVal = self.livePeerStatus[peer.displayName] 475 | self.livePeerStatus[peer.displayName] = true 476 | if oldVal == false { 477 | self.stateDelegate?.didChangeRoster() 478 | } 479 | } 480 | 481 | // if we're disconnected, then consider this a reconnection 482 | self.state = .live 483 | 484 | guard self.backgroundTask == UIBackgroundTaskInvalid else { 485 | self.logDelegate?.logString("HPN: WARNING, got potato in background, passing it off quickly...") 486 | self.passPotato() 487 | return 488 | } 489 | 490 | self.potatoDelegate?.youHaveThePotato(potato: self.potato!, finishBlock: { (potato) in 491 | self.potato = potato 492 | self.passPotato() 493 | }) 494 | } 495 | 496 | // MARK: 497 | // MARK: BUILDGRAPH - used on disconnects to ping and find out who is still around 498 | // MARK: 499 | 500 | fileprivate func sendBuildGraphHotPotatoMessage() { 501 | guard state == .disconnect else { 502 | return 503 | } 504 | 505 | self.missingPeersFromLiveList = self.livePeerNames.map { $0.key } 506 | self.missingPeersFromLiveList = self.missingPeersFromLiveList!.filter { // filter out paused peers 507 | if self.livePeerStatus[$0] == true { 508 | return true 509 | } else { 510 | self.logDelegate?.logString("HPN: sendBuildGraphHotPotatoMessage(), ignoring paused peer \($0)") 511 | return false 512 | } 513 | } 514 | // if all other peers are paused, then i'll pass the potato to myself 515 | guard self.missingPeersFromLiveList!.count != 0 else { 516 | self.state = .live 517 | self.startPotatoNow() 518 | return 519 | } 520 | 521 | if let index = self.missingPeersFromLiveList!.index(of: bluepeer!.displayNameSanitized) { 522 | self.missingPeersFromLiveList!.remove(at: index) // remove myself from the list of missing peers 523 | } 524 | let _ = bluepeer!.connectedPeers().map { // remove peers im already connected to 525 | if let index = self.missingPeersFromLiveList!.index(of: $0.displayName) { 526 | self.missingPeersFromLiveList!.remove(at: index) 527 | } 528 | } 529 | 530 | guard bluepeer!.connectedPeers().count > 0 else { 531 | self.logDelegate?.logString("HPN: sendBuildGraphHotPotatoMessage(), missing peers: \(self.missingPeersFromLiveList!.joined(separator: ", "))\nNOT CONNECTED to anything, so showing kickoutImmediately") 532 | self.prepareToDropPeers() 533 | return 534 | } 535 | 536 | self.logDelegate?.logString("HPN: sendBuildGraphHotPotatoMessage(), missing peers: \(self.missingPeersFromLiveList!.joined(separator: ", "))") 537 | 538 | // important: don't call sendRecoverHotPotatoMessage already even if there are no missing peers, because we might be the only one disconnected from a fully-connnected network. Wait for responses first. 539 | self.buildGraphHotPotatoMessageID = messageID + 1 540 | let buildgraph = BuildGraphHotPotatoMessage.init(myConnectedPeers: bluepeer!.connectedPeers().map({ $0.displayName }), myState: state, livePeerNames: self.livePeerNames, ID: self.buildGraphHotPotatoMessageID) 541 | sendHotPotatoMessage(message: buildgraph, replyBlock: nil) 542 | } 543 | 544 | fileprivate func sendRecoverHotPotatoMessage(withLivePeers: [String:Int64]) { 545 | if state != .disconnect { 546 | self.logDelegate?.logString("HPN: WARNING, skipping sendRecoverHotPotatoMessage() since we're not state=disconnect!") 547 | return 548 | } 549 | 550 | self.logDelegate?.logString("HPN: sendRecoverHotPotatoMessage, withLivePeers: \(withLivePeers)") 551 | self.livePeerNames = withLivePeers 552 | assert(livePeerNames.count > 0, "ERROR") 553 | self.state = .live // get .live before sending out to others. 554 | 555 | // send RecoverHotPotatoMessage, with live peers 556 | messageID += 1 557 | let recover = RecoverHotPotatoMessage.init(ID: messageID, livePeerNames: withLivePeers) 558 | 559 | // don't just call handleRecoverHotPotatoMessage, as we want to ensure the recoverMessage arrives at the remote side before the actual potato does (in the case where we win the startPotato election) 560 | sendHotPotatoMessage(message: recover, replyBlock: nil) 561 | startPotatoNow() 562 | } 563 | 564 | fileprivate func handleBuildGraphHotPotatoMessage(message: BuildGraphHotPotatoMessage, peer: BPPeer) { 565 | self.logDelegate?.logString("HPN: handleBuildGraphHotPotatoMessage from \(peer.displayName). remoteConnectedPeers: \(message.myConnectedPeers!.joined(separator: ", ")), state=\(String(describing: State(rawValue: message.myState!.rawValue)!)), livePeerNames: \(message.livePeerNames!)") 566 | if message.ID! == self.buildGraphHotPotatoMessageID { 567 | self.logDelegate?.logString("... handling \(peer.displayName)'s response to my BuildGraphHotPotatoMessage") 568 | 569 | if state != .disconnect { 570 | self.logDelegate?.logString("HPN: state!=disconnect, so no-op") 571 | return 572 | } 573 | if message.myState! != .disconnect { // expectation: they've seen the potato recently and we're connected to them, so just wait for potato to be passed to me 574 | self.logDelegate?.logString("HPN: \(peer.displayName)'s state!=disconnect, so no-op") 575 | return 576 | } 577 | 578 | guard var missing = self.missingPeersFromLiveList else { 579 | assert(false, "ERROR") 580 | return 581 | } 582 | if let index = missing.index(of: peer.displayName) { 583 | missing.remove(at: index) 584 | } 585 | let _ = message.myConnectedPeers!.map { // remote side's connected peers 586 | if let index = missing.index(of: $0) { 587 | missing.remove(at: index) 588 | } 589 | } 590 | self.missingPeersFromLiveList = missing 591 | if self.missingPeersFromLiveList!.count == 0 { 592 | // restart the potato 593 | self.sendRecoverHotPotatoMessage(withLivePeers: self.livePeerNames) 594 | } else { 595 | self.prepareToDropPeers() 596 | } 597 | } else { 598 | self.logDelegate?.logString("... replying to BuildGraphHotPotatoMessage from \(peer.displayName)") 599 | let reply = BuildGraphHotPotatoMessage.init(myConnectedPeers: bluepeer!.connectedPeers().map({ $0.displayName }), myState: state, livePeerNames: self.livePeerNames, ID: message.ID!) 600 | sendHotPotatoMessage(message: reply, toPeer: peer, replyBlock: nil) 601 | } 602 | } 603 | 604 | fileprivate func prepareToDropPeers() { 605 | // tell delegate the updated list 606 | self.stateDelegate?.thesePeersAreMissing(peerNames: self.missingPeersFromLiveList!, dropBlock: { 607 | // this code will get run if these peers are supposed to be dropped. 608 | var updatedLivePeers = self.livePeerNames 609 | let _ = self.missingPeersFromLiveList!.map { 610 | updatedLivePeers.removeValue(forKey: $0) 611 | self.livePeerStatus.removeValue(forKey: $0) 612 | } 613 | self.sendRecoverHotPotatoMessage(withLivePeers: updatedLivePeers) 614 | }, keepWaitingBlock: { 615 | // this code will get run if the user wants to keep waiting 616 | self.sendBuildGraphHotPotatoMessage() 617 | }) 618 | } 619 | 620 | fileprivate func handleRecoverHotPotatoMessage(message: RecoverHotPotatoMessage, peer: BPPeer?) { 621 | let name = peer?.displayName ?? "myself" 622 | self.livePeerNames = message.livePeerNames! 623 | self.logDelegate?.logString("Got handleRecoverHotPotatoMessage from \(name), new livePeers are \(message.livePeerNames!), going live and starting potato now") 624 | if state != .disconnect { 625 | self.logDelegate?.logString("handleRecoverHotPotatoMessage: not .disconnect, no-op") 626 | return 627 | } 628 | self.state = .live 629 | startPotatoNow() 630 | } 631 | 632 | fileprivate func handlePauseMeMessage(message: PauseMeMessage, peer: BPPeer?) { 633 | if message.ID! == pauseMeMessageID { 634 | // it's a reply to mine. 635 | if message.isPause == true { 636 | // I'm pausing. Once i've seen enough responses, I just end my bgTask. 637 | pauseMeMessageResponsesSeen += 1 638 | if pauseMeMessageResponsesSeen >= pauseMeMessageNumExpectedResponses { 639 | DispatchQueue.main.asyncAfter(deadline: .now()+2.0, execute: { // some extra time to get the last potato out 640 | self.logDelegate?.logString("handlePauseMeMessage: got all responses, ending BGTask") 641 | UIApplication.shared.endBackgroundTask(self.backgroundTask) 642 | // don't set backgroundTask to invalid, as we want potatoes to get handed off without processing until we foreground 643 | }) 644 | } else { 645 | self.logDelegate?.logString("handlePauseMeMessage: \(pauseMeMessageResponsesSeen) responses out of \(pauseMeMessageNumExpectedResponses)") 646 | } 647 | } else { 648 | // i'm unpausing. Update my live peer list 649 | assert(message.livePeerNames != nil, "ERROR") 650 | guard let _ = livePeerNames[bluepeer!.displayNameSanitized] else { // shouldn't be possible 651 | self.logDelegate?.logString("handlePauseMeMessage: ERROR, i'm not in the livepeers in unpause response") 652 | assert(false, "ERROR") 653 | state = .disconnect 654 | return 655 | } 656 | 657 | self.livePeerNames = message.livePeerNames! 658 | self.logDelegate?.logString("handlePauseMeMessage: unpause response received, updated livePeerNames") 659 | self.stateDelegate?.didChangeRoster() // in case there were comings/goings while we were asleep 660 | } 661 | } else { 662 | // it's from someone else: let's respond 663 | guard let _ = self.livePeerNames[peer!.displayName] else { 664 | self.logDelegate?.logString("handlePauseMeMessage: !!!!!!!!!! received a pause/unpause message from a peer not in livePeerNames, IGNORING IT") 665 | return 666 | } 667 | self.livePeerStatus[peer!.displayName] = !message.isPause! 668 | // if this is a response to an unpause, send my list of livepeers along so remote side is up to date 669 | if message.isPause == false { 670 | message.livePeerNames = self.livePeerNames 671 | } 672 | self.logDelegate?.logString("handlePauseMeMessage: HPN SERVICE TO \(peer!.displayName) IS \(message.isPause! ? "PAUSED" : "RESUMED")") 673 | self.sendHotPotatoMessage(message: message, toPeer: peer!, replyBlock: nil) // send reply as acknowledgement 674 | self.stateDelegate?.didChangeRoster() 675 | } 676 | } 677 | 678 | // MARK: 679 | // MARK: STARTING 680 | // MARK: 681 | 682 | open func startNetwork() -> Bool { 683 | let connectedPeersCount = bluepeer!.connectedPeers().count 684 | if connectedPeersCount == 0 { 685 | self.logDelegate?.logString("Aborting startButton - no connected peers") 686 | return false 687 | } 688 | 689 | self.logDelegate?.logString("Sending initial start message, \(connectedPeersCount) connected peers") 690 | self.startRepliesReceived = 0 691 | self.livePeersOnStart = connectedPeersCount 692 | messageID += 1 693 | self.startHotPotatoMessageID = messageID 694 | let startHotPotatoMessage = StartHotPotatoMessage(remoteDevices: connectedPeersCount, dataVersion: self.dataVersion, ID: messageID, livePeerNames: nil) 695 | sendHotPotatoMessage(message: startHotPotatoMessage, replyBlock: nil) 696 | return true 697 | } 698 | 699 | fileprivate func handleStartHotPotatoMessage(startHotPotatoMessage: StartHotPotatoMessage, peer: BPPeer) { 700 | self.logDelegate?.logString("Received StartHotPotatoMessage, ID: \(String(describing: startHotPotatoMessage.ID))") 701 | if state != .buildup { 702 | self.logDelegate?.logString("WARNING - ignoring startHotPotatoMessage because state != .buildup") 703 | return 704 | } 705 | 706 | if startHotPotatoMessage.dataVersion! != self.dataVersion { 707 | let dateformatter = DateFormatter.init() 708 | dateformatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssSSSSZZZZZ" 709 | let remoteVersionDate = dateformatter.date(from: startHotPotatoMessage.dataVersion!)! 710 | let myVersionDate = dateformatter.date(from: self.dataVersion)! 711 | self.stateDelegate?.showError(type: .versionMismatch, title: "Error", message: "This device has \(myVersionDate < remoteVersionDate ? "an earlier" : "a more recent") version of the data payload than \(peer.displayName).") 712 | self.logDelegate?.logString("WARNING - data version mismatch, disconnecting \(peer.displayName)") 713 | 714 | if startHotPotatoMessage.versionMismatchDetected == false { 715 | // so that the other side can detect mismatch also 716 | let returnMessage = startHotPotatoMessage 717 | returnMessage.dataVersion = self.dataVersion 718 | returnMessage.versionMismatchDetected = true 719 | self.sendHotPotatoMessage(message: returnMessage, toPeer: peer, replyBlock: nil) 720 | } 721 | peer.disconnect() 722 | return 723 | } 724 | 725 | // go live message - regardless of whether i pressed start or someone else did 726 | if let livePeers = startHotPotatoMessage.livePeerNames { 727 | if let _ = livePeers[bluepeer!.displayNameSanitized] { 728 | livePeerNames = livePeers 729 | for peer in livePeers { 730 | self.livePeerStatus[peer.key] = true // active 731 | } 732 | self.state = .live 733 | self.logDelegate?.logString("Received StartHotPotatoMessage GO LIVE from \(peer.displayName), set livePeerNames to \(livePeers)") 734 | startPotatoNow() 735 | } else { 736 | self.logDelegate?.logString("Received StartHotPotatoMessage GO LIVE from \(peer.displayName), but I AM NOT INCLUDED IN \(livePeers), disconnecting") 737 | peer.disconnect() 738 | } 739 | return 740 | } 741 | 742 | if startHotPotatoMessage.ID! == self.startHotPotatoMessageID { 743 | // this is a reply to me sending START. Looking to see number of replies (ie. from n-1 peers), that we all see the same number of peers, and that the data version matches 744 | 745 | if startHotPotatoMessage.remoteDevices! != self.livePeersOnStart || self.livePeersOnStart != bluepeer!.connectedPeers().count { 746 | self.logDelegate?.logString("WARNING - remote peer count mismatch, or my connCount has changed since start pressed") 747 | self.stateDelegate?.showError(type: .startClientCountMismatch, title: "Try Again", message: "Please try again when all devices are connected to each other") 748 | return 749 | } 750 | 751 | self.startRepliesReceived += 1 752 | if self.startRepliesReceived == self.livePeersOnStart { 753 | // got all the replies 754 | self.logDelegate?.logString("Received StartHotPotatoMessage reply from \(peer.displayName), checking customData then going LIVE and telling everyone") 755 | 756 | let completion = { 757 | self.livePeerNames[self.bluepeer!.displayNameSanitized] = Int64(self.deviceIdentifier.hashValue) // add self. 758 | self.livePeerStatus[self.bluepeer!.displayNameSanitized] = true // i'm active 759 | self.state = .live 760 | self.messageID += 1 761 | assert(self.livePeerNames.count > 0, "ERROR") 762 | self.logDelegate?.logString("Live peer list has been set to: \(self.livePeerNames)") 763 | let golive = StartHotPotatoMessage(remoteDevices: self.livePeersOnStart, dataVersion: self.dataVersion, ID: self.messageID, livePeerNames: self.livePeerNames) 764 | self.sendHotPotatoMessage(message: golive, replyBlock: nil) 765 | self.startPotatoNow() 766 | } 767 | 768 | let check = { () -> Bool in 769 | for peer in self.bluepeer!.connectedPeers() { 770 | if let peerId = peer.customData["id"] as? String { 771 | let peerIdInt = Int64(peerId)! 772 | self.livePeerNames[peer.displayName] = peerIdInt 773 | self.livePeerStatus[peer.displayName] = true // active 774 | } else { 775 | self.logDelegate?.logString("WARNING, FOUND PEER WITH NO CUSTOM DATA[id]: \(peer.displayName)") 776 | return false 777 | } 778 | } 779 | self.logDelegate?.logString("All customData accounted for.") 780 | return true 781 | } 782 | 783 | // if we received the reply before the TXT data had time to percolate, then wait until it's here 784 | if check() == true { 785 | completion() 786 | } else { 787 | DispatchQueue.main.asyncAfter(deadline: .now()+1.5, execute: { 788 | if check() == true { 789 | completion() 790 | } else { 791 | DispatchQueue.main.asyncAfter(deadline: .now()+1.5, execute: { 792 | if check() == true { 793 | completion() 794 | } else { 795 | assert(false, "ERROR still no customData") 796 | self.stateDelegate?.showError(type: .noCustomData, title: "Try Again", message: "Please wait a moment longer after all devices are connected.") 797 | } 798 | }) 799 | } 800 | }) 801 | } 802 | } else { 803 | self.logDelegate?.logString("Received StartHotPotatoMessage reply from \(peer.displayName), waiting for \(self.livePeersOnStart - self.startRepliesReceived) more") 804 | } 805 | } else { 806 | // someone else hit START - reply to them with what I know 807 | let reply = StartHotPotatoMessage(remoteDevices: bluepeer!.connectedPeers().count, dataVersion: self.dataVersion, ID: startHotPotatoMessage.ID!, livePeerNames: nil) 808 | sendHotPotatoMessage(message: reply, toPeer: peer, replyBlock: nil) 809 | } 810 | } 811 | } 812 | 813 | extension HotPotatoNetwork : BluepeerMembershipAdminDelegate { 814 | public func bluepeer(_ bluepeerObject: BluepeerObject, peerConnectionRequest peer: BPPeer, invitationHandler: @escaping (Bool) -> Void) { 815 | if self.connectionAllowedFrom(peer: peer) == false { 816 | invitationHandler(false) 817 | } else { 818 | invitationHandler(true) 819 | } 820 | } 821 | 822 | public func bluepeer(_ bluepeerObject: BluepeerObject, browserFoundPeer role: RoleType, peer: BPPeer) { 823 | self.connectToPeer(peer) 824 | } 825 | } 826 | 827 | extension HotPotatoNetwork : BluepeerMembershipRosterDelegate { 828 | public func bluepeer(_ bluepeerObject: BluepeerObject, peerDidConnect peerRole: RoleType, peer: BPPeer) { 829 | if let block = self.onConnectUnpauseBlock { 830 | if block() { // send unpause message 831 | self.onConnectUnpauseBlock = nil 832 | } 833 | } 834 | if state == .disconnect { 835 | self.sendBuildGraphHotPotatoMessage() 836 | } 837 | self.stateDelegate?.didChangeRoster() 838 | } 839 | 840 | public func bluepeer(_ bluepeerObject: BluepeerObject, peerDidDisconnect peerRole: RoleType, peer: BPPeer, canConnectNow: Bool) { 841 | self.stateDelegate?.didChangeRoster() 842 | if canConnectNow { 843 | self.logDelegate?.logString("HPN: peerDidDisconnect, canConnectNow - reconnecting...") 844 | self.connectToPeer(peer) 845 | } 846 | } 847 | 848 | public func bluepeer(_ bluepeerObject: BluepeerObject, peerConnectionAttemptFailed peerRole: RoleType, peer: BPPeer?, isAuthRejection: Bool, canConnectNow: Bool) { 849 | self.logDelegate?.logString("HPN: peerConnectionAttemptFailed for \(String(describing: peer?.displayName))!") 850 | if (isAuthRejection) { 851 | self.logDelegate?.logString("HPN: peerConnectionAttemptFailed, AuthRejection!") 852 | } 853 | if let peer = peer, canConnectNow == true { 854 | DispatchQueue.main.asyncAfter(deadline: .now() + 4.0, execute: { // otherwise it eats 100% CPU when looping fast 855 | self.logDelegate?.logString("HPN: peerConnectionAttemptFailed, canConnectNow - reconnecting...") 856 | self.connectToPeer(peer) 857 | }) 858 | } 859 | } 860 | } 861 | 862 | extension HotPotatoNetwork : BluepeerDataDelegate { 863 | public func bluepeer(_ bluepeerObject: BluepeerObject, didReceiveData data: Data, peer: BPPeer) { 864 | if state != .buildup && self.livePeerNames[peer.displayName] == nil { 865 | assert(false, "should not be possible") 866 | self.logDelegate?.logString("HPN didReceiveData: **** received data from someone not in livePeer list, IGNORING") 867 | return 868 | } 869 | 870 | if data.count > self.payloadHeader.count { 871 | let mightBePayloadHeader = data.subdata(in: 0..().map(JSONString: stringReceived) else { 885 | assert(false, "ERROR: received something that isn't UTF8 string, or can't map the string") 886 | return 887 | } 888 | 889 | let key = message.classNameAsString() 890 | self.logDelegate?.logString("Received message of type \(key)") 891 | 892 | // HotPotatoMessages I handle 893 | if let potatoHotPotatoMessage = message as? PotatoHotPotatoMessage { 894 | self.pendingPotatoHotPotatoMessage = potatoHotPotatoMessage 895 | return 896 | } else if let startmessage = message as? StartHotPotatoMessage { 897 | self.handleStartHotPotatoMessage(startHotPotatoMessage: startmessage, peer: peer) 898 | return 899 | } else if let buildgraphmessage = message as? BuildGraphHotPotatoMessage { 900 | self.handleBuildGraphHotPotatoMessage(message: buildgraphmessage, peer: peer) 901 | return 902 | } else if let recovermessage = message as? RecoverHotPotatoMessage { 903 | self.handleRecoverHotPotatoMessage(message: recovermessage, peer: peer) 904 | return 905 | } else if let pausemessage = message as? PauseMeMessage { 906 | self.handlePauseMeMessage(message: pausemessage, peer: peer) 907 | return 908 | } 909 | 910 | // HotPotatoMessages handled elsewhere 911 | if var queue = self.messageReplyQueue[key], let replyBlock = queue.dequeue() { 912 | //self.logDelegate?.logString("Found handler in messageReplyQueue, using that") 913 | replyBlock(message) 914 | } else if let handler = self.messageHandlers[key], handler.isActiveAsHandler == true { 915 | //self.logDelegate?.logString("Found handler in messageHandlers, using that") 916 | handler.handleHotPotatoMessage(message: message, peer: peer, HPN: self) 917 | } else { 918 | assert(false, "ERROR: unhandled message - \(message)") 919 | } 920 | } 921 | } 922 | 923 | public extension DateFormatter { 924 | class func ISO8601DateFormatter() -> DateFormatter { 925 | let retval = DateFormatter.init() 926 | retval.dateFormat = "yyyy-MM-dd'T'HH:mm:ssSSSSZZZZZ" 927 | return retval 928 | } 929 | } 930 | 931 | public extension Date { 932 | // warning, english only 933 | func relativeTimeStringFromNow() -> String { 934 | let dateComponents = Calendar.current.dateComponents([.minute, .hour, .day, .weekOfYear], from: self, to: Date.init()) 935 | if dateComponents.weekOfYear! == 0 && dateComponents.day! == 0 && dateComponents.hour! == 0 { 936 | if dateComponents.minute! <= 1 { 937 | return "just now" 938 | } 939 | // show minutes 940 | return "\(dateComponents.minute!) minute(s) ago" 941 | } else if dateComponents.weekOfYear! == 0 && dateComponents.day! == 0 { 942 | // show hours 943 | return "\(dateComponents.hour!) hour(s) ago" 944 | } else if dateComponents.weekOfYear! < 2 { 945 | // show days 946 | let days = dateComponents.weekOfYear! * 7 + dateComponents.day! 947 | return "\(days) day(s) ago" 948 | } else { 949 | // show weeks 950 | return "\(dateComponents.weekOfYear!) week(s) ago" 951 | } 952 | } 953 | } 954 | 955 | -------------------------------------------------------------------------------- /Core/BluepeerObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BluepeerObject.swift 3 | // 4 | // Created by Tim Carr on 7/7/16. 5 | // Copyright © 2016 Tim Carr Photo. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import CoreBluetooth 10 | import CFNetwork 11 | import CocoaAsyncSocket // NOTE: requires pod CocoaAsyncSocket 12 | import HHServices 13 | import dnssd 14 | import xaphodObjCUtils 15 | import DataCompression 16 | 17 | // "Any" means can be both a client and a server. If accepting a new connection from another peer, that peer is deemed a client. If found an advertising .any peer, that peer is considered a server. 18 | @objc public enum RoleType: Int, CustomStringConvertible { 19 | case unknown = 0 20 | case server = 1 21 | case client = 2 22 | case any = 3 23 | 24 | public var description: String { 25 | switch self { 26 | case .server: return "Server" 27 | case .client: return "Client" 28 | case .unknown: return "Unknown" 29 | case .any: return "Any" 30 | } 31 | } 32 | 33 | public static func roleFromString(_ roleStr: String?) -> RoleType { 34 | guard let roleStr = roleStr else { 35 | assert(false, "ERROR") 36 | return .unknown 37 | } 38 | switch roleStr { 39 | case "Server": return .server 40 | case "Client": return .client 41 | case "Unknown": return .unknown 42 | case "Any": return .any 43 | default: return .unknown 44 | } 45 | } 46 | } 47 | 48 | @objc public enum BluetoothState: Int { 49 | case unknown = 0 50 | case poweredOff = 1 51 | case poweredOn = 2 52 | case other = 3 53 | } 54 | 55 | @objc public enum BluepeerInterfaces: Int { 56 | case any = 0 57 | case infrastructureModeWifiOnly 58 | case notWifi 59 | } 60 | 61 | 62 | @objc public enum BPPeerState: Int, CustomStringConvertible { 63 | case notConnected = 0 64 | case connecting = 1 65 | case awaitingAuth = 2 66 | case authenticated = 3 67 | 68 | public var description: String { 69 | switch self { 70 | case .notConnected: return "NotConnected" 71 | case .connecting: return "Connecting" 72 | case .awaitingAuth: return "AwaitingAuth" 73 | case .authenticated: return "Authenticated" 74 | } 75 | } 76 | } 77 | 78 | @objc open class BPPeer: NSObject { 79 | @objc open var displayName: String = "" // is same as HHService.name ! 80 | @objc open var displayShortName: String { // doesn't have the "-0923" numbers at the end 81 | if displayName.count <= 5 { 82 | return displayName 83 | } 84 | let index = displayName.index(displayName.endIndex, offsetBy: -5) 85 | return String(displayName.prefix(upTo: index)) 86 | } 87 | @objc open var role: RoleType = .unknown 88 | @objc open var state: BPPeerState = .notConnected 89 | @objc open var canConnect: Bool { 90 | return self.candidateAddresses.count > 0 91 | } 92 | @objc open var keepaliveTimer: Timer? 93 | @objc open var lastDataRead: Date = Date.init(timeIntervalSince1970: 0) // includes keepalives 94 | @objc open var lastDataKeepAliveWritten: Date = Date.init(timeIntervalSince1970: 0) // only keepalives 95 | @objc open var lastDataNonKeepAliveWritten: Date = Date.init(timeIntervalSince1970: 0) // no keepalives 96 | public typealias ConnectBlock = ()->Bool 97 | @objc open var connect: (ConnectBlock)? // false if no connection attempt will happen 98 | @objc open func disconnect() { 99 | socket?.disconnect() 100 | } 101 | @objc open var customData = [String:AnyHashable]() // store your own data here. When a browser finds an advertiser and creates a peer for it, this will be filled out with the advertiser's customData *even before any connection occurs*. Note that while you can store values that are AnyHashable, only Strings are used as values for advertiser->browser connections. 102 | weak var owner: BluepeerObject? 103 | 104 | var connectCount=0, disconnectCount=0, connectAttemptFailCount=0, connectAttemptFailAuthRejectCount=0, dataRecvCount=0, dataSendCount=0 105 | override open var description: String { 106 | var socketDesc = "nil" 107 | if let socket = self.socket { 108 | if let host = socket.connectedHost { 109 | socketDesc = host 110 | } else { 111 | socketDesc = "NOT connected" 112 | } 113 | } 114 | let lastDataWritten = max(self.lastDataNonKeepAliveWritten, self.lastDataKeepAliveWritten) // the most recent 115 | return "\n[\(displayName) is \(state) as \(role) on \(lastInterfaceName ?? "nil") with \(customData.count) customData keys. C:\(connectCount) D:\(disconnectCount) cFail:\(connectAttemptFailCount) cFailAuth:\(connectAttemptFailAuthRejectCount), Data In#:\(dataRecvCount) LastRecv: \(lastDataRead), LastWrite: \(lastDataWritten), Out#:\(dataSendCount). Socket: \(socketDesc), services#: \(services.count) (\(resolvedServices().count) resolved)]" 116 | } 117 | @objc open var isConnectedViaWifi: Bool { 118 | guard let interface = self.lastInterfaceName else { 119 | return false 120 | } 121 | return interface == BluepeerObject.iOS_wifi_interface 122 | } 123 | 124 | // fileprivates 125 | fileprivate var socket: GCDAsyncSocket? 126 | fileprivate var services = [HHService]() 127 | fileprivate func resolvedServices() -> [HHService] { 128 | return services.filter { $0.resolved == true } 129 | } 130 | fileprivate func pickedResolvedService() -> HHService? { 131 | // this function determines which of the resolved services is used 132 | return resolvedServices().last 133 | } 134 | fileprivate func destroyServices() { 135 | for service in services { 136 | service.delegate = nil 137 | service.endResolve() 138 | } 139 | services = [HHService]() 140 | } 141 | fileprivate var candidateAddresses: [HHAddressInfo] { 142 | guard let hhaddresses2 = self.pickedResolvedService()?.resolvedAddressInfo, hhaddresses2.count > 0 else { 143 | return [] 144 | } 145 | var interfaces: BluepeerInterfaces = .any 146 | if let owner = self.owner { 147 | interfaces = owner.bluepeerInterfaces 148 | } 149 | return hhaddresses2.filter({ $0.isCandidateAddress(excludeWifi: interfaces == .notWifi, onlyWifi: interfaces == .infrastructureModeWifiOnly) }) 150 | } 151 | 152 | fileprivate var lastInterfaceName: String? 153 | fileprivate var clientReceivedBytes: Int = 0 154 | } 155 | 156 | @objc public protocol BluepeerMembershipRosterDelegate { 157 | @objc optional func bluepeer(_ bluepeerObject: BluepeerObject, peerDidConnect peerRole: RoleType, peer: BPPeer) 158 | @objc optional func bluepeer(_ bluepeerObject: BluepeerObject, peerDidDisconnect peerRole: RoleType, peer: BPPeer, canConnectNow: Bool) // canConnectNow: true if this peer is still announce-able, ie. can now call connect() on it. Note, it is highly recommended to have a ~2 sec delay before calling connect() to avoid 100% CPU loops 159 | @objc optional func bluepeer(_ bluepeerObject: BluepeerObject, peerConnectionAttemptFailed peerRole: RoleType, peer: BPPeer?, isAuthRejection: Bool, canConnectNow: Bool) // canConnectNow: true if this peer is still announce-able, ie. can now call connect() on it. Note, it is highly recommended to have a ~2 sec delay before calling connect() to avoid 100% CPU loops 160 | } 161 | 162 | @objc public protocol BluepeerMembershipAdminDelegate { 163 | @objc optional func bluepeer(_ bluepeerObject: BluepeerObject, peerConnectionRequest peer: BPPeer, invitationHandler: @escaping (Bool) -> Void) // Someone's trying to connect to you. Earlier this was named: sessionConnectionRequest 164 | @objc optional func bluepeer(_ bluepeerObject: BluepeerObject, browserFindingPeer isNew: Bool) // There's someone out there with the same serviceType, which is now being queried for more details. This can occur as much as 2 seconds before browserFoundPeer(), so it's used to give you an early heads-up. If this peer has not been seen before, isNew is true. 165 | @objc optional func bluepeer(_ bluepeerObject: BluepeerObject, browserFindingPeerFailed unused: Bool) // balances the previous call. Use to cancel UI like progress indicator etc. 166 | @objc optional func bluepeer(_ bluepeerObject: BluepeerObject, browserFoundPeer role: RoleType, peer: BPPeer) // You found someone to connect to. The peer has connect() that can be executed, and your .customData too. This can be called more than once for the same peer. 167 | @objc optional func bluepeer(_ bluepeerObject: BluepeerObject, browserLostPeer role: RoleType, peer: BPPeer) 168 | } 169 | 170 | @objc public protocol BluepeerDataDelegate { 171 | @objc optional func bluepeer(_ bluepeerObject: BluepeerObject, receivingData bytesReceived: Int, totalBytes: Int, peer: BPPeer) // the values of 0 and 100% are guaranteed prior to didReceiveData 172 | @objc func bluepeer(_ bluepeerObject: BluepeerObject, didReceiveData data: Data, peer: BPPeer) 173 | } 174 | 175 | 176 | @objc public protocol BluepeerLoggingDelegate { 177 | @objc func logString(_ message: String) 178 | } 179 | 180 | /* ADD TO POD FILE 181 | post_install do |installer_representation| 182 | installer_representation.pods_project.targets.each do |target| 183 | target.build_configurations.each do |config| 184 | if config.name != 'Release' 185 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)', 'DEBUG=1'] 186 | config.build_settings['OTHER_SWIFT_FLAGS'] = ['$(inherited)', '-DDEBUG'] 187 | end 188 | end 189 | end 190 | end 191 | */ 192 | 193 | 194 | @objc open class BluepeerObject: NSObject { 195 | static let kDNSServiceInterfaceIndexP2PSwift = UInt32.max-2 196 | static let iOS_wifi_interface = "en0" 197 | 198 | @objc var delegateQueue: DispatchQueue? 199 | @objc var serverSocket: GCDAsyncSocket? 200 | @objc var publisher: HHServicePublisher? 201 | @objc var browser: HHServiceBrowser? 202 | fileprivate var bluepeerInterfaces: BluepeerInterfaces = .any 203 | open var advertisingRole: RoleType? 204 | @objc var advertisingCustomData = [String:String]() 205 | @objc open var browsing: Bool = false 206 | var onLastBackground: (advertising: RoleType?, browsing: Bool) = (nil, false) 207 | @objc open var serviceType: String = "" 208 | @objc var serverPort: UInt16 = 0 209 | @objc var versionString: String = "unknown" 210 | @objc open var displayNameSanitized: String = "" 211 | @objc var appIsInBackground = false 212 | 213 | @objc weak open var membershipAdminDelegate: BluepeerMembershipAdminDelegate? 214 | @objc weak open var membershipRosterDelegate: BluepeerMembershipRosterDelegate? 215 | @objc weak open var dataDelegate: BluepeerDataDelegate? 216 | @objc weak open var logDelegate: BluepeerLoggingDelegate? { 217 | didSet { 218 | fileLogDelegate = logDelegate 219 | } 220 | } 221 | @objc open var peers = [BPPeer]() // does not include self 222 | @objc open var bluetoothState : BluetoothState = .unknown 223 | @objc var bluetoothPeripheralManager: CBPeripheralManager! 224 | @objc open var bluetoothBlock: ((_ bluetoothState: BluetoothState) -> Void)? 225 | @objc open var disconnectOnBackground: Bool = false 226 | let headerTerminator: Data = "\r\n\r\n".data(using: String.Encoding.utf8)! // same as HTTP. But header content here is just a number, representing the byte count of the incoming nsdata. 227 | let keepAliveHeader: Data = "0 ! 0 ! 0 ! 0 ! 0 ! 0 ! 0 ! ".data(using: String.Encoding.utf8)! // A special header kept to avoid timeouts 228 | let socketQueue = DispatchQueue(label: "xaphod.bluepeer.socketQueue", attributes: []) 229 | @objc var browsingWorkaroundRestarts = 0 230 | 231 | /// nil : no compression 232 | /// ZLIB : Fast with a very solid compression rate. There is a reason it is used everywhere. 233 | /// LZFSE : Apples proprietary compression algorithm. Claims to compress as good as ZLIB but 2 to 3 times faster. 234 | /// LZMA : Horribly slow. Compression as well as decompression. Normally you will regret choosing LZMA. 235 | /// LZ4 : Fast, but depending on the data the compression rate can be really bad. Which is often the case. 236 | open var compressionAlgorithm: Data.CompressionAlgorithm? = .lzfse 237 | @objc open func turnOffCompression() { self.compressionAlgorithm = nil } 238 | 239 | enum DataTag: Int { 240 | case tag_HEADER = -1 241 | // case tag_BODY = -2 -- no longer used: negative tag values are conventional, positive tag values indicate number of bytes expected to read 242 | case tag_WRITING = -3 243 | case tag_AUTH = -4 244 | case tag_NAME = -5 245 | case tag_WRITINGKEEPALIVE = -6 // new in 1.4.0 246 | } 247 | 248 | enum Timeouts: Double { 249 | case header = 40 250 | case body = 90 251 | case keepAlive = 16 252 | } 253 | 254 | fileprivate var fileLogDelegate: BluepeerLoggingDelegate? 255 | 256 | func dlog(_ items: CustomStringConvertible...) { 257 | var willLog = false 258 | #if DEBUG 259 | willLog = true 260 | #endif 261 | if let _ = fileLogDelegate { 262 | willLog = true 263 | } 264 | guard willLog else { return } 265 | 266 | var str = "" 267 | for item in items { 268 | str += item.description 269 | } 270 | let serviceType = self.serviceType.replacingOccurrences(of: "_xd-", with: "").replacingOccurrences(of: "._tcp", with: "") 271 | 272 | if let del = fileLogDelegate { 273 | del.logString("Bluepeer \(serviceType) " + str) 274 | } else { 275 | let formatter = DateFormatter.init() 276 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSS" 277 | let out = formatter.string(from: Date.init()) + " - Bluepeer \(serviceType) - " + str 278 | print(out) 279 | } 280 | } 281 | 282 | // if queue isn't given, main queue is used 283 | @objc public init?(serviceType: String, displayName:String?, queue:DispatchQueue?, serverPort: UInt16, interfaces: BluepeerInterfaces, logDelegate: BluepeerLoggingDelegate? = nil, bluetoothBlock: ((_ bluetoothState: BluetoothState)->Void)?) { 284 | 285 | super.init() 286 | fileLogDelegate = logDelegate 287 | 288 | if #available(iOS 13.1, *) { 289 | dlog("CBManager.authorization = \(CBManager.authorization.debugDescription)") 290 | } 291 | 292 | // serviceType must be 1-15 chars, only a-z0-9 and hyphen, eg "xd-blueprint" 293 | if serviceType.count > 15 { 294 | assert(false, "ERROR: service name is too long") 295 | return nil 296 | } 297 | self.serviceType = "_" + self.sanitizeCharsToDNSChars(str: serviceType) + "._tcp" 298 | 299 | self.serverPort = serverPort 300 | self.delegateQueue = queue 301 | self.bluepeerInterfaces = interfaces 302 | 303 | var name = UIDevice.current.name 304 | if let displayName = displayName { 305 | name = displayName 306 | } 307 | self.displayNameSanitized = self.sanitizeStringAsDNSName(str: name) 308 | 309 | if let bundleVersionString = Bundle.main.infoDictionary?["CFBundleVersion"] as? String { 310 | versionString = bundleVersionString 311 | } 312 | 313 | self.bluetoothBlock = bluetoothBlock 314 | self.bluetoothPeripheralManager = CBPeripheralManager.init(delegate: self, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey:0]) 315 | NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) 316 | NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) 317 | if #available(iOS 8.2, *) { 318 | NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackground), name: NSNotification.Name.NSExtensionHostDidEnterBackground, object: nil) 319 | NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground), name: NSNotification.Name.NSExtensionHostWillEnterForeground, object: nil) 320 | } 321 | 322 | dlog("Initialized. Name: \(self.displayNameSanitized)") 323 | } 324 | 325 | deinit { 326 | NotificationCenter.default.removeObserver(self) 327 | self.killAllKeepaliveTimers() // probably moot: won't start deiniting until all timers are dead, because they have a strong ref to self 328 | self.disconnectSession() 329 | self.stopBrowsing() 330 | self.stopAdvertising() 331 | dlog("DEINIT FINISH") 332 | } 333 | 334 | override open var description: String { 335 | let formatter = DateFormatter.init() 336 | formatter.dateStyle = .none 337 | formatter.timeStyle = .medium 338 | var retval = "" 339 | for peer in self.peers { 340 | retval += peer.description + "\n" 341 | } 342 | return retval 343 | } 344 | 345 | // Note: if I disconnect, then my delegate is expected to reconnect if needed. 346 | @objc func didEnterBackground() { 347 | self.appIsInBackground = true 348 | self.onLastBackground = (self.advertisingRole, self.browsing) 349 | stopBrowsing() 350 | stopAdvertising() 351 | if disconnectOnBackground { 352 | disconnectSession() 353 | dlog("didEnterBackground - stopped browsing & advertising, and disconnected session") 354 | } else { 355 | dlog("didEnterBackground - stopped browsing & advertising") 356 | } 357 | } 358 | 359 | @objc func willEnterForeground() { 360 | dlog("willEnterForeground") 361 | self.appIsInBackground = false 362 | if let role = self.onLastBackground.advertising { 363 | dlog("willEnterForeground, startAdvertising") 364 | startAdvertising(role, customData: self.advertisingCustomData) 365 | } 366 | if self.onLastBackground.browsing { 367 | dlog("willEnterForeground, startBrowsing") 368 | startBrowsing() 369 | } 370 | } 371 | 372 | func sanitizeCharsToDNSChars(str: String) -> String { 373 | let acceptableChars = CharacterSet.init(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-") 374 | return String(str.filter({ $0 != "'"}).map { (char: Character) in 375 | if acceptableChars.containsCharacter(char) == false { 376 | return "-" 377 | } else { 378 | return char 379 | } 380 | }) 381 | } 382 | 383 | // per DNSServiceRegister called by HHServices, the String must be at most 63 bytes of UTF8 data. But, Bluepeer sends 32-byte name strings as part of its connection buildup, so limit this to max 32 bytes. The name is used to uniquely identify devices, and devices might have the same name (ie. "iPhone"), so append some random numbers to the end 384 | // returns: the given string with an identifier appended on the end, guaranteed to be max 32 bytes 385 | func sanitizeStringAsDNSName(str: String) -> String { 386 | // first sanitize name by getting rid of invalid chars 387 | var retval = self.sanitizeCharsToDNSChars(str: str) 388 | 389 | // we need to append "-NNNN" to the end, which is 5 bytes in UTF8. So, the name part can be max 32-5 bytes = 27 bytes 390 | // characters take up different amounts of bytes in UTF8, so let's be careful to count them 391 | var strData = Data.init(capacity: 32) 392 | 393 | // make 27-byte UTF-8 name 394 | for c in retval { 395 | if let thisData = String.init(c).data(using: String.Encoding.utf8) { 396 | if thisData.count + (strData.count) <= 27 { 397 | strData.append(thisData) 398 | } else { 399 | break 400 | } 401 | } else { 402 | assert(false, "ERROR: non-UTF8 chars found") 403 | } 404 | } 405 | 406 | strData.append(String.init("-").data(using: String.Encoding.utf8)!) 407 | for _ in 1...4 { 408 | strData.append(String.init(arc4random_uniform(10)).data(using: String.Encoding.utf8)!) 409 | } 410 | 411 | retval = String.init(data: strData, encoding: String.Encoding.utf8)! 412 | dlog("sanitized \(str) to \(retval)") 413 | return retval 414 | } 415 | 416 | @objc open func disconnectSession() { 417 | // don't close serverSocket: expectation is that only stopAdvertising does this 418 | // loop through peers, disconenct all sockets 419 | for peer in self.peers { 420 | dlog(" disconnectSession: disconnecting \(peer.displayName)") 421 | peer.socket?.synchronouslySetDelegate(nil) // we don't want to run our disconnection logic below 422 | peer.socket?.disconnect() 423 | peer.socket = nil 424 | peer.destroyServices() 425 | peer.state = .notConnected 426 | peer.keepaliveTimer?.invalidate() 427 | peer.keepaliveTimer = nil 428 | } 429 | self.peers = [] // remove all peers! 430 | } 431 | 432 | @objc open func connectedPeers(_ role: RoleType) -> [BPPeer] { 433 | return self.peers.filter({ $0.role == role && $0.state == .authenticated }) 434 | } 435 | 436 | @objc open func connectedPeers() -> [BPPeer] { 437 | return self.peers.filter({ $0.state == .authenticated }) 438 | } 439 | 440 | // specify customData if this is needed for browser to decide whether to connect or not. Each key and value should be less than 255 bytes, and the total should be less than 1300 bytes. 441 | @objc open func startAdvertising(_ role: RoleType, customData: [String:String]) { 442 | if let _ = self.advertisingRole { 443 | dlog("Already advertising (no-op)") 444 | return 445 | } 446 | 447 | dlog("starting advertising using port \(serverPort)") 448 | 449 | // type must be like: _myexampleservice._tcp (no trailing .) 450 | // txtData, from http://www.zeroconf.org/rendezvous/txtrecords.html: Using TXT records larger than 1300 bytes is NOT RECOMMENDED at this time. The format of the data within a DNS TXT record is zero or more strings, packed together in memory without any intervening gaps or padding bytes for word alignment. The format of each constituent string within the DNS TXT record is a single length byte, followed by 0-255 bytes of text data. 451 | 452 | // Could use the NSNetService version of this (TXTDATA maker), it'd be easier :) 453 | var swiftdict: [String:String] = ["role":role.description, "comp":"1"] // 1.3.0: added compression flag to advertising/browsing, to make sure old clients cannot connect to new ones due to differing compression algorithms 454 | self.advertisingCustomData = customData 455 | swiftdict.merge(with: customData) 456 | 457 | let cfdata: Unmanaged? = CFNetServiceCreateTXTDataWithDictionary(kCFAllocatorDefault, swiftdict as CFDictionary) 458 | guard let txtdata = cfdata?.takeUnretainedValue() else { 459 | dlog("ERROR could not create TXTDATA") 460 | return 461 | } 462 | self.publisher = HHServicePublisher.init(name: self.displayNameSanitized, type: self.serviceType, domain: "local.", txtData: txtdata as Data, port: UInt(serverPort)) 463 | self.publisher?.mainDispatchQueue = socketQueue 464 | 465 | guard let publisher = self.publisher else { 466 | dlog("could not create publisher") 467 | return 468 | } 469 | publisher.delegate = self 470 | var starting: Bool 471 | 472 | switch self.bluepeerInterfaces { 473 | case .any: 474 | starting = publisher.beginPublish() 475 | break 476 | case .notWifi: 477 | starting = publisher.beginPublishOverBluetoothOnly() 478 | break 479 | case .infrastructureModeWifiOnly: 480 | starting = publisher.beginPublish(UInt32(kDNSServiceInterfaceIndexAny), includeP2P: false) 481 | break 482 | } 483 | 484 | if !starting { 485 | dlog("ERROR could not start advertising") 486 | assert(false, "Check Info.plist: add NSBonjourServices and NSLocalNetworkUsageDescription entries if missing") 487 | self.publisher = nil 488 | self.advertisingRole = nil 489 | return 490 | } 491 | // serverSocket is created in didPublish delegate (from HHServicePublisherDelegate) below 492 | self.advertisingRole = role 493 | } 494 | 495 | @objc open func stopAdvertising(leaveServerSocketAlone: Bool = false) { 496 | if let _ = self.advertisingRole { 497 | if let publisher = self.publisher { 498 | publisher.endPublish() 499 | } else { 500 | dlog("WARNING: publisher is MIA while advertising set true!") 501 | } 502 | dlog("advertising stopped") 503 | } else { 504 | dlog("no advertising to stop (no-op)") 505 | } 506 | self.publisher = nil 507 | self.advertisingRole = nil 508 | if leaveServerSocketAlone == false { 509 | self.destroyServerSocket() 510 | } 511 | } 512 | 513 | fileprivate func createServerSocket() -> Bool { 514 | self.serverSocket = GCDAsyncSocket.init(delegate: self, delegateQueue: socketQueue) 515 | self.serverSocket?.isIPv4PreferredOverIPv6 = false 516 | guard let serverSocket = self.serverSocket else { 517 | dlog("ERROR - Could not create serverSocket") 518 | return false 519 | } 520 | 521 | do { 522 | try serverSocket.accept(onPort: serverPort) 523 | } catch { 524 | dlog("ERROR accepting on serverSocket") 525 | return false 526 | } 527 | 528 | dlog("Created serverSocket, is accepting on \(serverPort)") 529 | return true 530 | } 531 | 532 | fileprivate func destroyServerSocket() { 533 | if let socket = self.serverSocket { 534 | socket.synchronouslySetDelegate(nil) 535 | socket.disconnect() 536 | self.serverSocket = nil 537 | dlog("Destroyed serverSocket") 538 | } 539 | } 540 | 541 | @objc open func startBrowsing() { 542 | if self.browsing == true { 543 | dlog("Already browsing (no-op)") 544 | return 545 | } 546 | 547 | self.browser = HHServiceBrowser.init(type: self.serviceType, domain: "local.") 548 | self.browser?.mainDispatchQueue = socketQueue 549 | guard let browser = self.browser else { 550 | dlog("ERROR, could not create browser") 551 | return 552 | } 553 | browser.delegate = self 554 | 555 | switch self.bluepeerInterfaces { 556 | case .any: 557 | self.browsing = browser.beginBrowse() 558 | break 559 | case .notWifi: 560 | self.browsing = browser.beginBrowseOverBluetoothOnly() 561 | break 562 | case .infrastructureModeWifiOnly: 563 | self.browsing = browser.beginBrowse(UInt32(kDNSServiceInterfaceIndexAny), includeP2P: false) 564 | break 565 | } 566 | 567 | dlog("now browsing") 568 | } 569 | 570 | @objc open func stopBrowsing() { 571 | if (self.browsing) { 572 | if self.browser == nil { 573 | dlog("WARNING, browser is MIA while browsing set true! ") 574 | } else { 575 | self.browser!.endBrowse() 576 | dlog("browsing stopped") 577 | } 578 | } else { 579 | dlog("no browsing to stop") 580 | } 581 | self.browser = nil 582 | self.browsing = false 583 | for peer in self.peers { 584 | peer.destroyServices() 585 | } 586 | } 587 | 588 | @objc open func sendData(_ datas: [Data], toPeers:[BPPeer]) throws { 589 | for data in datas { 590 | for peer in toPeers { 591 | self.sendDataInternal(peer, data: data) 592 | } 593 | } 594 | } 595 | 596 | @objc open func sendData(_ datas: [Data], toRole: RoleType) throws { 597 | let targetPeers: [BPPeer] = peers.filter({ 598 | if toRole != .any { 599 | return $0.role == toRole && $0.state == .authenticated 600 | } else { 601 | return $0.state == .authenticated 602 | } 603 | }) 604 | 605 | for data in datas { 606 | if data.count == 0 { 607 | continue 608 | } 609 | for peer in targetPeers { 610 | self.sendDataInternal(peer, data: data) 611 | } 612 | } 613 | } 614 | 615 | func sendDataInternal(_ peer: BPPeer, data: Data) { 616 | // send header first. Then separator. Then send body. 617 | // length: send as 4-byte, then 4 bytes of unused 0s for now. assumes endianness doesn't change between platforms, ie 23 00 00 00 not 00 00 00 23 618 | 619 | // compress that data 620 | 621 | var dataToSend: Data 622 | var ratio: Double = 100.0 623 | var timeToCompress: Double = 0.0 624 | if let algorithm = self.compressionAlgorithm { 625 | let compressionStart = Date.init() 626 | guard let compressedData = data.compress(withAlgorithm: algorithm) else { 627 | assert(false) 628 | return 629 | } 630 | timeToCompress = abs(compressionStart.timeIntervalSinceNow) 631 | ratio = Double(compressedData.count) / Double(data.count) * 100.0 632 | dataToSend = compressedData 633 | } else { 634 | dataToSend = data 635 | } 636 | var length: UInt = UInt(dataToSend.count) 637 | let senddata = NSMutableData.init(bytes: &length, length: 4) 638 | let unused4bytesdata = NSMutableData.init(length: 4)! 639 | senddata.append(unused4bytesdata as Data) 640 | senddata.append(self.headerTerminator) 641 | senddata.append(dataToSend) 642 | dlog("sending \(senddata.length) bytes to \(peer.displayName): \(data.count) bytes compressed to \(dataToSend.count) bytes (\(ratio)%) in \(timeToCompress)s") 643 | 644 | 645 | // DLog("sendDataInternal writes: \((senddata as Data).hex), payload part: \(data.hex)") 646 | peer.dataSendCount += 1 647 | peer.socket?.write(senddata as Data, withTimeout: Timeouts.body.rawValue, tag: DataTag.tag_WRITING.rawValue) 648 | } 649 | 650 | func scheduleNextKeepaliveTimer(_ peer: BPPeer) { 651 | DispatchQueue.main.async { [weak self] in 652 | guard let self = self else { return } 653 | if peer.state != .authenticated || peer.socket == nil { 654 | return 655 | } 656 | 657 | peer.dataRecvCount += 1 658 | if peer.dataRecvCount + 1 == Int.max { 659 | peer.dataRecvCount = 0 660 | } 661 | 662 | if peer.keepaliveTimer?.isValid == true { 663 | return 664 | } 665 | 666 | let delay: TimeInterval = Timeouts.keepAlive.rawValue - 5 - (Double(arc4random_uniform(5000)) / Double(1000)) // keepAlive.rawValue - 5 - (up to 5) 667 | self.dlog("keepalive INITIAL SCHEDULING for \(peer.displayName) in \(delay)s") 668 | peer.keepaliveTimer = Timer.scheduledTimer(timeInterval: delay, target: self, selector: #selector(self.keepAliveTimerFired), userInfo: ["peer":peer], repeats: true) 669 | } 670 | } 671 | 672 | @objc func keepAliveTimerFired(_ timer: Timer) { 673 | guard let ui = timer.userInfo as? Dictionary else { 674 | assert(false, "ERROR") 675 | timer.invalidate() 676 | return 677 | } 678 | guard let peer = ui["peer"] as? BPPeer else { 679 | dlog("keepAlive timer didn't find a peer, invalidating timer") 680 | timer.invalidate() 681 | return 682 | } 683 | if peer.state != .authenticated || peer.socket == nil { 684 | dlog("keepAlive timer finds peer isn't authenticated(connected), invalidating timer") 685 | timer.invalidate() 686 | peer.keepaliveTimer = nil 687 | return 688 | } 689 | 690 | // New in 1.4.0: send keepalives OR real data sends, not both at same time 691 | let timeKeepAlives = abs(peer.lastDataKeepAliveWritten.timeIntervalSinceNow) 692 | let timeNotKeepAlives = abs(peer.lastDataNonKeepAliveWritten.timeIntervalSinceNow) 693 | assert(Thread.current.isMainThread) 694 | guard timeNotKeepAlives > Timeouts.keepAlive.rawValue / 2.0 else { 695 | dlog("keepAlive timer no-op as data was recently sent: timeKeepAlive=\(timeKeepAlives), timeNotKeepAlive=\(timeNotKeepAlives)") 696 | return 697 | } 698 | var senddata = NSData.init(data: self.keepAliveHeader) as Data 699 | senddata.append(self.headerTerminator) 700 | dlog("writeKeepAlive to \(peer.displayName)") 701 | peer.socket?.write(senddata, withTimeout: Timeouts.header.rawValue, tag: DataTag.tag_WRITINGKEEPALIVE.rawValue) 702 | } 703 | 704 | func killAllKeepaliveTimers() { 705 | for peer in self.peers { 706 | peer.keepaliveTimer?.invalidate() 707 | peer.keepaliveTimer = nil 708 | } 709 | } 710 | 711 | func dispatch_on_delegate_queue(_ block: @escaping ()->()) { 712 | if let queue = self.delegateQueue { 713 | queue.async(execute: block) 714 | } else { 715 | DispatchQueue.main.async(execute: block) 716 | } 717 | } 718 | 719 | @objc open func getBrowser(_ completionBlock: @escaping (Bool) -> ()) -> UIViewController? { 720 | let initialVC = self.getStoryboard()?.instantiateInitialViewController() 721 | var browserVC = initialVC 722 | if let nav = browserVC as? UINavigationController { 723 | browserVC = nav.topViewController 724 | } 725 | guard let browser = browserVC as? BluepeerBrowserViewController else { 726 | assert(false, "ERROR - storyboard layout changed") 727 | return nil 728 | } 729 | browser.bluepeerObject = self 730 | browser.browserCompletionBlock = completionBlock 731 | return initialVC 732 | } 733 | 734 | func getStoryboard() -> UIStoryboard? { 735 | guard let bundlePath = Bundle.init(for: BluepeerObject.self).path(forResource: "Bluepeer", ofType: "bundle") else { 736 | assert(false, "ERROR: could not load bundle") 737 | return nil 738 | } 739 | return UIStoryboard.init(name: "Bluepeer", bundle: Bundle.init(path: bundlePath)) 740 | } 741 | } 742 | 743 | extension GCDAsyncSocket { 744 | var peer: BPPeer? { 745 | guard let bo = self.delegate as? BluepeerObject else { 746 | return nil 747 | } 748 | guard let peer = bo.peers.filter({ $0.socket == self }).first else { 749 | return nil 750 | } 751 | return peer 752 | } 753 | } 754 | 755 | extension BluepeerObject : HHServicePublisherDelegate { 756 | public func serviceDidPublish(_ servicePublisher: HHServicePublisher) { 757 | // create serverSocket 758 | if let socket = self.serverSocket { 759 | dlog("serviceDidPublish: USING EXISTING SERVERSOCKET \(socket)") 760 | } else { 761 | if self.createServerSocket() == false { 762 | return 763 | } 764 | } 765 | 766 | dlog("now advertising for service \(serviceType)") 767 | } 768 | 769 | public func serviceDidNotPublish(_ servicePublisher: HHServicePublisher) { 770 | self.advertisingRole = nil 771 | dlog("ERROR: serviceDidNotPublish") 772 | } 773 | } 774 | 775 | extension BluepeerObject : HHServiceBrowserDelegate { 776 | public func serviceBrowser(_ serviceBrowser: HHServiceBrowser, didFind service: HHService, moreComing: Bool) { 777 | if self.browsing == false { 778 | return 779 | } 780 | if self.displayNameSanitized == service.name { // names are made unique by having random numbers appended 781 | dlog("found my own published service, ignoring...") 782 | return 783 | } 784 | 785 | if service.type == self.serviceType { 786 | dlog("didFindService \(service.name), moreComing: \(moreComing)") 787 | service.delegate = self 788 | 789 | // if this peer exists, then add this as another address(es), otherwise add now 790 | var peer = self.peers.filter({ $0.displayName == service.name }).first 791 | 792 | if let peer = peer { 793 | dlog("didFind: added new unresolved service to peer \(peer.displayName)") 794 | peer.services.append(service) 795 | self.dispatch_on_delegate_queue({ 796 | self.membershipAdminDelegate?.bluepeer?(self, browserFindingPeer: false) 797 | }) 798 | } else { 799 | peer = BPPeer.init() 800 | guard let peer = peer else { return } 801 | peer.owner = self 802 | peer.displayName = service.name 803 | peer.services.append(service) 804 | self.peers.append(peer) 805 | dlog("didFind: created new peer \(peer.displayName). Peers(n=\(self.peers.count)) after adding") 806 | self.dispatch_on_delegate_queue({ 807 | self.membershipAdminDelegate?.bluepeer?(self, browserFindingPeer: true) 808 | }) 809 | } 810 | 811 | let prots = UInt32(kDNSServiceProtocol_IPv4) | UInt32(kDNSServiceProtocol_IPv6) 812 | switch self.bluepeerInterfaces { 813 | case .any: 814 | service.beginResolve(UInt32(kDNSServiceInterfaceIndexAny), includeP2P: true, addressLookupProtocols: prots) 815 | break 816 | case .notWifi: 817 | service.beginResolve(BluepeerObject.kDNSServiceInterfaceIndexP2PSwift, includeP2P: true, addressLookupProtocols: prots) 818 | break 819 | case .infrastructureModeWifiOnly: 820 | service.beginResolve(UInt32(kDNSServiceInterfaceIndexAny), includeP2P: false, addressLookupProtocols: prots) 821 | break 822 | } 823 | } 824 | } 825 | 826 | public func serviceBrowser(_ serviceBrowser: HHServiceBrowser, didRemove service: HHService, moreComing: Bool) { 827 | let matchingPeers = self.peers.filter({ $0.displayName == service.name }) 828 | if matchingPeers.count == 0 { 829 | dlog("didRemoveService for service.name \(service.name) -- IGNORING because no peer found") 830 | return 831 | } 832 | for peer in matchingPeers { 833 | // if found exact service, then nil it out to prevent more connection attempts 834 | if let serviceIndex = peer.services.firstIndex(of: service) { 835 | let previousResolvedServiceCount = peer.resolvedServices().count 836 | dlog("didRemoveService - REMOVING SERVICE from \(peer.displayName)") 837 | peer.services.remove(at: serviceIndex) 838 | 839 | if peer.resolvedServices().count == 0 && previousResolvedServiceCount > 0 { 840 | dlog("didRemoveService - that was the LAST resolved service so calling browserLostPeer for \(peer.displayName)") 841 | self.dispatch_on_delegate_queue({ 842 | self.membershipAdminDelegate?.bluepeer?(self, browserLostPeer: peer.role, peer: peer) 843 | }) 844 | } 845 | } else { 846 | dlog("didRemoveService - \(peer.displayName) has no matching service (no-op)") 847 | } 848 | } 849 | } 850 | } 851 | 852 | extension BluepeerObject : HHServiceDelegate { 853 | public func serviceDidResolve(_ service: HHService, moreComing: Bool) { 854 | func cleanup() { 855 | self.dispatch_on_delegate_queue({ 856 | self.membershipAdminDelegate?.bluepeer?(self, browserFindingPeerFailed: false) 857 | }) 858 | } 859 | 860 | if self.browsing == false { 861 | return 862 | } 863 | 864 | guard let txtdata = service.txtData else { 865 | dlog("serviceDidResolve IGNORING service because no txtData found") 866 | cleanup() 867 | return 868 | } 869 | guard let hhaddresses: [HHAddressInfo] = service.resolvedAddressInfo, hhaddresses.count > 0 else { 870 | dlog("serviceDidResolve IGNORING service because could not get resolvedAddressInfo") 871 | cleanup() 872 | return 873 | } 874 | 875 | let cfdict: Unmanaged? = CFNetServiceCreateDictionaryWithTXTData(kCFAllocatorDefault, txtdata as CFData) 876 | guard let _ = cfdict else { 877 | dlog("serviceDidResolve IGNORING service because txtData was invalid") 878 | cleanup() 879 | return 880 | } 881 | let dict: NSDictionary = cfdict!.takeUnretainedValue() 882 | guard let rD = dict["role"] as? Data, 883 | let roleStr = String.init(data: rD, encoding: String.Encoding.utf8) else { 884 | dlog("serviceDidResolve IGNORING service because role was missing") 885 | cleanup() 886 | return 887 | } 888 | let role = RoleType.roleFromString(roleStr) 889 | switch role { 890 | case .unknown: 891 | assert(false, "Expecting a role that isn't unknown here") 892 | cleanup() 893 | return 894 | default: 895 | break 896 | } 897 | 898 | // load custom data 899 | var customData = [String:String]() 900 | for (k, v) in dict { 901 | guard let k = k as? String, 902 | let vd = v as? Data, 903 | let v = String.init(data: vd, encoding: String.Encoding.utf8) else { 904 | assert(false, "Expecting [String:Data] dict") 905 | continue 906 | } 907 | 908 | if k == "role" { 909 | continue 910 | } else { 911 | customData[k] = v 912 | } 913 | } 914 | 915 | let matchingPeers = self.peers.filter({ $0.displayName == service.name }) 916 | if matchingPeers.count != 1 { 917 | dlog("serviceDidResolve FAILED, expected 1 peer but found \(matchingPeers.count)") 918 | assert(false) 919 | cleanup() 920 | return 921 | } 922 | let peer = matchingPeers.first! 923 | peer.role = role 924 | peer.customData = customData 925 | dlog("serviceDidResolve for \(peer.displayName) - addresses \(hhaddresses.count), peer.role: \(role), peer.state: \(peer.state), #customData:\(customData.count)") 926 | 927 | // if has insufficient version, don't announce it 928 | guard let compStr = customData["comp"], compStr == "1" else { 929 | dlog("serviceDidResolve: IGNORING THIS PEER, IT IS TOO OLD - DOES NOT SUPPORT CORRECT COMPRESSION ALGORITHM") 930 | cleanup() 931 | return 932 | } 933 | 934 | let candidates = hhaddresses.filter({ $0.isCandidateAddress(excludeWifi: self.bluepeerInterfaces == .notWifi, onlyWifi: self.bluepeerInterfaces == .infrastructureModeWifiOnly) }) 935 | if peer.state != .notConnected { 936 | dlog("... has state!=notConnected, so no-op") 937 | return 938 | } else if candidates.count == 0 { 939 | dlog("... no candidates out of \(hhaddresses.count) addresses. moreComing = \(moreComing)") 940 | if hhaddresses.count > 1 && !moreComing && self.bluepeerInterfaces == .notWifi { 941 | if (self.browsingWorkaroundRestarts >= 3) { 942 | dlog(" *** workarounds exhausted! falling back to not bluetoothOnly") 943 | self.browsingWorkaroundRestarts = 0 944 | self.bluepeerInterfaces = .any 945 | let secondaryCandidates = hhaddresses.filter({ $0.isCandidateAddress(excludeWifi: self.bluepeerInterfaces == .notWifi, onlyWifi: self.bluepeerInterfaces == .infrastructureModeWifiOnly) }) 946 | if (secondaryCandidates.count <= 0) { 947 | dlog("... STILL no candidates. Bummer.") 948 | return 949 | } 950 | } else { 951 | self.browsingWorkaroundRestarts += 1 952 | dlog(" *** workaround for BT radio connection delay: restarting browsing, \(self.browsingWorkaroundRestarts) of 3") 953 | self.stopBrowsing() 954 | if let indexOfPeer = self.peers.firstIndex(of: peer) { 955 | self.peers.remove(at: indexOfPeer) 956 | } 957 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 958 | self.startBrowsing() 959 | } 960 | return 961 | } 962 | } else { 963 | return 964 | } 965 | } 966 | 967 | peer.connect = { [weak self] in 968 | guard let self = self else { return false } 969 | do { 970 | // pick the address to connect to INSIDE the block, because more addresses may have been added between announcement and inviteBlock being executed 971 | let candidateAddresses = peer.candidateAddresses 972 | guard candidateAddresses.count > 0 else { 973 | self.dlog("connect: \(peer.displayName) has no candidates, BAILING") 974 | return false 975 | } 976 | 977 | // We have N candidateAddresses from 1 resolved service, which should we pick? 978 | // if we tried to connect to one recently and failed, and there's another one, pick the other one 979 | var interimAddress: HHAddressInfo? 980 | if let lastInterfaceName = peer.lastInterfaceName, candidateAddresses.count > 1 { 981 | let otherAddresses = candidateAddresses.filter { $0.interfaceName != lastInterfaceName } 982 | if otherAddresses.count > 0 { 983 | interimAddress = otherAddresses.first! 984 | self.dlog("connect: \(peer.displayName) trying a different interface than last attempt. Now trying: \(interimAddress!.interfaceName)") 985 | } 986 | } 987 | 988 | var chosenAddress = candidateAddresses.first! 989 | if let interimAddress = interimAddress { 990 | // first priority is not always trying to reconnect on the same interface when >1 is available 991 | chosenAddress = interimAddress 992 | } else { 993 | // second priority is if we can use wifi, then do so 994 | if self.bluepeerInterfaces != .notWifi && !chosenAddress.isWifiInterface() { 995 | let wifiCandidates = candidateAddresses.filter({ $0.isWifiInterface() }) 996 | if wifiCandidates.count > 0 { 997 | chosenAddress = wifiCandidates.first! 998 | } 999 | } 1000 | } 1001 | 1002 | peer.lastInterfaceName = chosenAddress.interfaceName 1003 | self.dlog("connect: \(peer.displayName) has \(peer.services.count) svcs (\(peer.resolvedServices().count) resolved); chose \(chosenAddress.addressAndPortString) on interface \(chosenAddress.interfaceName)") 1004 | let sockdata = chosenAddress.socketAsData() 1005 | 1006 | if let oldSocket = peer.socket { 1007 | self.dlog("**** connect: PEER ALREADY HAD SOCKET, DESTROYING...") 1008 | oldSocket.synchronouslySetDelegate(nil) 1009 | oldSocket.disconnect() 1010 | peer.socket = nil 1011 | } 1012 | 1013 | peer.socket = GCDAsyncSocket.init(delegate: self, delegateQueue: self.socketQueue) 1014 | peer.socket?.isIPv4PreferredOverIPv6 = false 1015 | peer.state = .connecting 1016 | try peer.socket?.connect(toAddress: sockdata, withTimeout: 10.0) 1017 | // try peer.socket?.connect(toAddress: sockdata, viaInterface: chosenAddress.interfaceName, withTimeout: 10.0) 1018 | return true 1019 | } catch { 1020 | self.dlog("could not connect, ERROR: \(error)") 1021 | peer.state = .notConnected 1022 | return false 1023 | } 1024 | } 1025 | 1026 | guard let delegate = self.membershipAdminDelegate else { 1027 | dlog("announcePeer: WARNING, no-op because membershipAdminDelegate is not set") 1028 | return 1029 | } 1030 | 1031 | if self.appIsInBackground == false { 1032 | dlog("announcePeer: announcing now with browserFoundPeer - call peer.connect() to connect") 1033 | self.dispatch_on_delegate_queue({ 1034 | delegate.bluepeer?(self, browserFoundPeer: peer.role, peer: peer) 1035 | }) 1036 | } else { 1037 | dlog("announcePeer: app is in BACKGROUND, no-op!") 1038 | } 1039 | } 1040 | 1041 | public func serviceDidNotResolve(_ service: HHService) { 1042 | dlog("****** ERROR, service did not resolve: \(service.name) *******") 1043 | self.dispatch_on_delegate_queue({ 1044 | self.membershipAdminDelegate?.bluepeer?(self, browserFindingPeerFailed: false) 1045 | }) 1046 | } 1047 | } 1048 | 1049 | extension HHAddressInfo { 1050 | func isIPV6() -> Bool { 1051 | let socketAddrData = Data.init(bytes: self.address, count: MemoryLayout.size) 1052 | var storage = sockaddr_storage() 1053 | (socketAddrData as NSData).getBytes(&storage, length: MemoryLayout.size) 1054 | if Int32(storage.ss_family) == AF_INET6 { 1055 | return true 1056 | } else { 1057 | return false 1058 | } 1059 | } 1060 | 1061 | func isWifiInterface() -> Bool { 1062 | return self.interfaceName == BluepeerObject.iOS_wifi_interface 1063 | } 1064 | 1065 | func socketAsData() -> Data { 1066 | var socketAddrData = Data.init(bytes: self.address, count: MemoryLayout.size) 1067 | var storage = sockaddr_storage() 1068 | (socketAddrData as NSData).getBytes(&storage, length: MemoryLayout.size) 1069 | if Int32(storage.ss_family) == AF_INET6 { 1070 | socketAddrData = Data.init(bytes: self.address, count: MemoryLayout.size) 1071 | } 1072 | return socketAddrData 1073 | } 1074 | 1075 | func isCandidateAddress(excludeWifi: Bool, onlyWifi: Bool) -> Bool { 1076 | if excludeWifi && self.isWifiInterface() { 1077 | return false 1078 | } 1079 | if onlyWifi && !self.isWifiInterface() { 1080 | return false 1081 | } 1082 | if ProcessInfo().isOperatingSystemAtLeast(OperatingSystemVersion(majorVersion: 10, minorVersion: 0, patchVersion: 0)) { 1083 | // iOS 10: *require* IPv6 address! 1084 | return self.isIPV6() 1085 | } else { 1086 | return true 1087 | } 1088 | } 1089 | 1090 | func IP() -> String? { 1091 | let ipAndPort = self.addressAndPortString 1092 | guard let lastColonIndex = ipAndPort.range(of: ":", options: .backwards)?.lowerBound else { 1093 | assert(false, "error") 1094 | return nil 1095 | } 1096 | let ip = String(ipAndPort[.. 1 { 1098 | // ipv6 - looks like [00:22:22.....]:port 1099 | let start = ip.index(ip.startIndex, offsetBy: 1) 1100 | let end = ip.index(ip.endIndex, offsetBy: -1) 1101 | return String(ip[start ..< end]) 1102 | } 1103 | return ip 1104 | } 1105 | } 1106 | 1107 | 1108 | extension BluepeerObject : GCDAsyncSocketDelegate { 1109 | 1110 | public func socket(_ sock: GCDAsyncSocket, didAcceptNewSocket newSocket: GCDAsyncSocket) { 1111 | 1112 | if self.membershipAdminDelegate != nil { 1113 | guard let connectedHost = newSocket.connectedHost, let localhost = newSocket.localHost else { 1114 | dlog("ERROR, accepted newSocket has no connectedHost (no-op)") 1115 | return 1116 | } 1117 | newSocket.delegate = self 1118 | let newPeer = BPPeer.init() 1119 | newPeer.owner = self 1120 | newPeer.state = .awaitingAuth 1121 | newPeer.role = .client 1122 | newPeer.socket = newSocket 1123 | newPeer.lastInterfaceName = XaphodUtils.interfaceName(ofLocalIpAddress: localhost) 1124 | 1125 | self.peers.append(newPeer) // always add as a new peer, even if it already exists. This might result in a dupe if we are browsing and advertising for same service. The original will get removed on receiving the name of other device, if it matches 1126 | dlog("accepting new connection from \(connectedHost) on \(String(describing: newPeer.lastInterfaceName)). Peers(n=\(self.peers.count)) after adding") 1127 | 1128 | // CONVENTION: CLIENT sends SERVER 32 bytes of its name -- UTF-8 string 1129 | newSocket.readData(toLength: 32, withTimeout: Timeouts.header.rawValue, tag: DataTag.tag_NAME.rawValue) 1130 | } else { 1131 | dlog("WARNING, ignoring connection attempt because I don't have a membershipAdminDelegate assigned") 1132 | } 1133 | } 1134 | 1135 | public func socket(_ sock: GCDAsyncSocket, didConnectToHost host: String, port: UInt16) { 1136 | guard let peer = sock.peer else { 1137 | dlog("WARNING, did not find peer in didConnectToHost, doing nothing") 1138 | return 1139 | } 1140 | peer.state = .awaitingAuth 1141 | dlog("got to state = awaitingAuth with \(String(describing: sock.connectedHost)), sending name then awaiting ACK ('0')") 1142 | let strData = self.displayNameSanitized.data(using: String.Encoding.utf8)! as NSData 1143 | // Other side is expecting to receive EXACTLY 32 bytes so pad to 32 bytes! 1144 | let paddedStrData: NSMutableData = (" ".data(using: String.Encoding.utf8) as NSData?)?.mutableCopy() as! NSMutableData // that's 32 spaces :) 1145 | paddedStrData.replaceBytes(in: NSMakeRange(0, strData.length), withBytes: strData.bytes) 1146 | 1147 | dlog("didConnect to \(String(describing: sock.connectedHost)), writing name: \((paddedStrData as Data).hex)") 1148 | sock.write(paddedStrData as Data, withTimeout: Timeouts.header.rawValue, tag: DataTag.tag_WRITING.rawValue) 1149 | 1150 | // now await auth 1151 | dlog("waiting to read Auth from \(String(describing: sock.connectedHost))...") 1152 | sock.readData(toLength: 1, withTimeout: Timeouts.header.rawValue, tag: DataTag.tag_AUTH.rawValue) 1153 | } 1154 | 1155 | public func socketDidDisconnect(_ sock: GCDAsyncSocket, withError err: Error?) { 1156 | let matchingPeers = self.peers.filter({ $0.socket == sock}) 1157 | if matchingPeers.count != 1 { 1158 | dlog(" socketDidDisconnect: WARNING expected to find 1 peer with this socket but found \(matchingPeers.count), calling peerConnectionAttemptFailed.") 1159 | sock.synchronouslySetDelegate(nil) 1160 | self.dispatch_on_delegate_queue({ 1161 | self.membershipRosterDelegate?.bluepeer?(self, peerConnectionAttemptFailed: .unknown, peer: nil, isAuthRejection: false, canConnectNow: false) 1162 | }) 1163 | return 1164 | } 1165 | let peer = matchingPeers.first! 1166 | dlog(" socketDidDisconnect: \(peer.displayName) disconnected. Peers(n=\(self.peers.count))") 1167 | let oldState = peer.state 1168 | peer.state = .notConnected 1169 | peer.keepaliveTimer?.invalidate() 1170 | peer.keepaliveTimer = nil 1171 | peer.socket = nil 1172 | sock.synchronouslySetDelegate(nil) 1173 | 1174 | switch oldState { 1175 | case .authenticated: 1176 | peer.disconnectCount += 1 1177 | self.dispatch_on_delegate_queue({ 1178 | self.membershipRosterDelegate?.bluepeer?(self, peerDidDisconnect: peer.role, peer: peer, canConnectNow: peer.canConnect) 1179 | }) 1180 | break 1181 | case .notConnected: 1182 | break 1183 | case .connecting, .awaitingAuth: 1184 | peer.connectAttemptFailCount += 1 1185 | if oldState == .awaitingAuth { 1186 | peer.connectAttemptFailAuthRejectCount += 1 1187 | } 1188 | self.dispatch_on_delegate_queue({ 1189 | self.membershipRosterDelegate?.bluepeer?(self, peerConnectionAttemptFailed: peer.role, peer: peer, isAuthRejection: oldState == .awaitingAuth, canConnectNow: peer.canConnect) 1190 | }) 1191 | break 1192 | } 1193 | } 1194 | 1195 | fileprivate func disconnectSocket(socket: GCDAsyncSocket?, peer: BPPeer) { 1196 | peer.state = .notConnected 1197 | socket?.synchronouslySetDelegate(nil) 1198 | socket?.disconnect() 1199 | peer.socket = nil 1200 | } 1201 | 1202 | public func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) { 1203 | guard let peer = sock.peer else { 1204 | dlog("WARNING, did not find peer in didReadData, doing nothing") 1205 | return 1206 | } 1207 | self.scheduleNextKeepaliveTimer(peer) 1208 | DispatchQueue.main.async { 1209 | peer.lastDataRead = Date.init() 1210 | } 1211 | 1212 | if tag == DataTag.tag_AUTH.rawValue { 1213 | if data.count != 1 { 1214 | assert(false, "ERROR: not right length of bytes") 1215 | self.disconnectSocket(socket: sock, peer: peer) 1216 | return 1217 | } 1218 | var ack: UInt8 = 1 1219 | (data as NSData).getBytes(&ack, length: 1) 1220 | if (ack != 0 || peer.state != .awaitingAuth) { 1221 | assert(false, "ERROR: not the right ACK, or state was not .awaitingAuth as expected") 1222 | self.disconnectSocket(socket: sock, peer: peer) 1223 | return 1224 | } 1225 | peer.state = .authenticated // CLIENT becomes authenticated 1226 | peer.connectCount += 1 1227 | self.dispatch_on_delegate_queue({ 1228 | self.membershipRosterDelegate?.bluepeer?(self, peerDidConnect: peer.role, peer: peer) 1229 | }) 1230 | dlog("\(peer.displayName).state=connected (auth OK), readHeaderTerminator1") 1231 | sock.readData(to: self.headerTerminator, withTimeout: Timeouts.header.rawValue, tag: DataTag.tag_HEADER.rawValue) 1232 | 1233 | } else if tag == DataTag.tag_HEADER.rawValue { 1234 | // first, strip the trailing headerTerminator 1235 | let range = 0.. 0 { 1274 | var newCustomData = peer.customData 1275 | if newCustomData.count > 0 { 1276 | newCustomData.merge(with: existingPeer.customData) 1277 | } else { 1278 | peer.customData = existingPeer.customData 1279 | } 1280 | } 1281 | peer.services.append(contentsOf: existingPeer.services) 1282 | existingPeer.keepaliveTimer?.invalidate() 1283 | existingPeer.keepaliveTimer = nil 1284 | self.disconnectSocket(socket: existingPeer.socket, peer: existingPeer) 1285 | self.peers.remove(at: indexOfPeer) 1286 | dlog("... removed") 1287 | } 1288 | 1289 | if let delegate = self.membershipAdminDelegate { 1290 | self.dispatch_on_delegate_queue({ 1291 | delegate.bluepeer?(self, peerConnectionRequest: peer, invitationHandler: { [weak self] (inviteAccepted) in 1292 | guard let self = self else { return } 1293 | if peer.state != .awaitingAuth || sock.isConnected != true { 1294 | self.dlog("inviteHandlerBlock: not connected/wrong state, so cannot accept!") 1295 | if (sock.isConnected) { 1296 | self.disconnectSocket(socket: sock, peer: peer) 1297 | } 1298 | } else if inviteAccepted { 1299 | peer.state = .authenticated // SERVER-local-peer becomes connected 1300 | peer.connectCount += 1 1301 | // CONVENTION: SERVER sends CLIENT a single 0 to show connection has been accepted, since it isn't possible to send a header for a payload of size zero except here. 1302 | sock.write(Data.init(count: 1), withTimeout: Timeouts.header.rawValue, tag: DataTag.tag_WRITING.rawValue) 1303 | self.dlog("inviteHandlerBlock: accepted \(peer.displayName) (by my delegate), reading header...") 1304 | self.dispatch_on_delegate_queue({ 1305 | self.membershipRosterDelegate?.bluepeer?(self, peerDidConnect: .client, peer: peer) 1306 | }) 1307 | self.scheduleNextKeepaliveTimer(peer) // NEW in 1.1: if the two sides open up a connection but no one says anything, make sure it stays open 1308 | sock.readData(to: self.headerTerminator, withTimeout: Timeouts.header.rawValue, tag: DataTag.tag_HEADER.rawValue) 1309 | } else { 1310 | self.dlog("inviteHandlerBlock: auth-rejected \(peer.displayName) (by my delegate)") 1311 | self.disconnectSocket(socket: sock, peer: peer) 1312 | } 1313 | }) 1314 | }) 1315 | } 1316 | 1317 | } else { // BODY/data case 1318 | var data = data 1319 | if let algorithm = self.compressionAlgorithm { 1320 | guard let uncompressedData = data.decompress(withAlgorithm: algorithm) else { 1321 | assert(false, "Could not decompress data") 1322 | return 1323 | } 1324 | data = uncompressedData 1325 | } 1326 | peer.clientReceivedBytes = 0 1327 | self.dispatch_on_delegate_queue({ 1328 | self.dataDelegate?.bluepeer?(self, receivingData: tag, totalBytes: tag, peer: peer) 1329 | self.dataDelegate?.bluepeer(self, didReceiveData: data, peer: peer) 1330 | }) 1331 | sock.readData(to: self.headerTerminator, withTimeout: Timeouts.header.rawValue, tag: DataTag.tag_HEADER.rawValue) 1332 | } 1333 | } 1334 | 1335 | public func socket(_ sock: GCDAsyncSocket, didReadPartialDataOfLength partialLength: UInt, tag: Int) { 1336 | guard let peer = sock.peer else { return } 1337 | self.scheduleNextKeepaliveTimer(peer) 1338 | DispatchQueue.main.async { 1339 | peer.lastDataRead = Date.init() 1340 | } 1341 | guard partialLength > 0 else { return } 1342 | peer.clientReceivedBytes += Int(partialLength) 1343 | self.dispatch_on_delegate_queue({ 1344 | self.dataDelegate?.bluepeer?(self, receivingData: peer.clientReceivedBytes, totalBytes: tag, peer: peer) 1345 | }) 1346 | } 1347 | 1348 | private func updateKeepAlivesOnSend(peer: BPPeer, tag: Int) { 1349 | DispatchQueue.main.async { 1350 | if tag == DataTag.tag_WRITINGKEEPALIVE.rawValue { 1351 | peer.lastDataKeepAliveWritten = Date.init() 1352 | } else { 1353 | peer.lastDataNonKeepAliveWritten = Date.init() 1354 | } 1355 | } 1356 | } 1357 | 1358 | // New in 1.4.0: avoid sending keepalives while sending data 1359 | public func socket(_ sock: GCDAsyncSocket, didWritePartialDataOfLength partialLength: UInt, tag: Int) { 1360 | guard let peer = sock.peer else { return } 1361 | updateKeepAlivesOnSend(peer: peer, tag: tag) 1362 | } 1363 | 1364 | // New in 1.4.0: avoid sending keepalives while sending data 1365 | public func socket(_ sock: GCDAsyncSocket, didWriteDataWithTag tag: Int) { 1366 | guard let peer = sock.peer else { return } 1367 | updateKeepAlivesOnSend(peer: peer, tag: tag) 1368 | } 1369 | 1370 | public func socket(_ sock: GCDAsyncSocket, shouldTimeoutReadWithTag tag: Int, elapsed: TimeInterval, bytesDone length: UInt) -> TimeInterval { 1371 | return self.calcTimeExtension(sock, tag: tag) 1372 | } 1373 | public func socket(_ sock: GCDAsyncSocket, shouldTimeoutWriteWithTag tag: Int, elapsed: TimeInterval, bytesDone length: UInt) -> TimeInterval { 1374 | return self.calcTimeExtension(sock, tag: tag) 1375 | } 1376 | 1377 | public func calcTimeExtension(_ sock: GCDAsyncSocket, tag: Int) -> TimeInterval { 1378 | guard let peer = sock.peer else { 1379 | return 0 1380 | } 1381 | 1382 | assert(!Thread.current.isMainThread) 1383 | // it can happen that while sending lots of data, we don't receive keepalives that the other side is sending. 1384 | // so if we are sending real data succesfully, don't time out 1385 | var timeSinceLastDataRead: Double? 1386 | var timeSinceLastDataWrittenWithoutKeepAlives: Double? 1387 | DispatchQueue.main.sync { 1388 | timeSinceLastDataRead = abs(peer.lastDataRead.timeIntervalSinceNow) 1389 | timeSinceLastDataWrittenWithoutKeepAlives = abs(peer.lastDataNonKeepAliveWritten.timeIntervalSinceNow) 1390 | } 1391 | guard let timeSinceRead = timeSinceLastDataRead, let timeSinceWrite = timeSinceLastDataWrittenWithoutKeepAlives else { 1392 | assert(false) 1393 | return Timeouts.keepAlive.rawValue 1394 | } 1395 | let timeSince = min(timeSinceRead, timeSinceWrite) 1396 | if timeSince > (2.0 * Timeouts.keepAlive.rawValue) { 1397 | // timeout! 1398 | dlog("keepalive: socket timed out waiting for read/write. timeSinceRead: \(timeSinceRead), timeSinceWrite: \(timeSinceWrite). Tag: \(tag). Disconnecting.") 1399 | sock.disconnect() 1400 | return 0 1401 | } 1402 | // extend 1403 | dlog("keepalive: extending socket timeout by \(Timeouts.keepAlive.rawValue)s bc I saw data \(timeSince)s ago") 1404 | return Timeouts.keepAlive.rawValue 1405 | } 1406 | } 1407 | 1408 | extension BluepeerObject: CBPeripheralManagerDelegate { 1409 | public func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) { 1410 | guard self.bluetoothPeripheralManager != nil else { 1411 | dlog("!!!!!! BluepeerObject bluetooth state change with no bluetoothPeripheralManager !!!!!") 1412 | return 1413 | } 1414 | dlog("Bluetooth status: ") 1415 | switch (self.bluetoothPeripheralManager.state) { 1416 | case .unknown: 1417 | dlog("Unknown") 1418 | self.bluetoothState = .unknown 1419 | case .resetting: 1420 | dlog("Resetting") 1421 | self.bluetoothState = .other 1422 | case .unsupported: 1423 | dlog("Unsupported") 1424 | self.bluetoothState = .other 1425 | case .unauthorized: 1426 | dlog("Unauthorized") 1427 | self.bluetoothState = .other 1428 | case .poweredOff: 1429 | dlog("PoweredOff") 1430 | self.bluetoothState = .poweredOff 1431 | case .poweredOn: 1432 | dlog("PoweredOn") 1433 | self.bluetoothState = .poweredOn 1434 | @unknown default: 1435 | dlog("WARNING: unknown Bluetooth state") 1436 | break 1437 | } 1438 | self.bluetoothBlock?(self.bluetoothState) 1439 | } 1440 | } 1441 | 1442 | extension CharacterSet { 1443 | func containsCharacter(_ c:Character) -> Bool { 1444 | let s = String(c) 1445 | let result = s.rangeOfCharacter(from: self) 1446 | return result != nil 1447 | } 1448 | } 1449 | 1450 | extension Data { 1451 | var hex: String { 1452 | return self.map { b in String(format: "%02X", b) }.joined() 1453 | } 1454 | } 1455 | 1456 | extension Dictionary { 1457 | mutating func merge(with a: Dictionary) { 1458 | for (k,v) in a { 1459 | self[k] = v 1460 | } 1461 | } 1462 | } 1463 | 1464 | @available(iOS 13.0, *) 1465 | extension CBManagerAuthorization : CustomDebugStringConvertible { 1466 | public var debugDescription: String { 1467 | switch self { 1468 | case .allowedAlways: return "allowedAlways" 1469 | case .denied: return "DENIED" 1470 | case .notDetermined: return "notDetermined" 1471 | case .restricted: return "RESTRICTED" 1472 | default: return "UNKNOWN!" 1473 | } 1474 | } 1475 | } 1476 | --------------------------------------------------------------------------------