yggdrasil-ios/Yggdrasil Network iOS/View Controllers/PeersViewController.swift
Neil Alexander 52ca049b50 Sanitised
2020-07-19 14:20:15 +01:00

312 lines
13 KiB
Swift

//
// PeersViewController.swift
// YggdrasilNetwork
//
// Created by Neil Alexander on 07/01/2019.
//
import UIKit
import NetworkExtension
import CoreTelephony
class PeersViewController: UITableViewController {
var app = UIApplication.shared.delegate as! AppDelegate
var config: [String: Any]? = nil
@IBOutlet var peerTable: UITableView!
@IBOutlet weak var addButtonItem: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
if let proto = self.app.vpnManager.protocolConfiguration as? NETunnelProviderProtocol {
config = proto.providerConfiguration ?? nil
}
self.navigationItem.rightBarButtonItems = [
self.editButtonItem,
self.addButtonItem
]
}
override func viewWillAppear(_ animated: Bool) {
NotificationCenter.default.addObserver(self, selector: #selector(self.onYggdrasilPeersUpdated), name: NSNotification.Name.YggdrasilPeersUpdated, object: nil)
}
override func viewWillDisappear(_ animated: Bool) {
NotificationCenter.default.removeObserver(self, name: NSNotification.Name.YggdrasilPeersUpdated, object: nil)
}
@objc func onYggdrasilPeersUpdated(notification: NSNotification) {
peerTable.reloadData()
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0: return app.yggdrasilSwitchPeers.count
case 1:
if let config = self.app.yggdrasilConfig {
if let peers = config.get("Peers") as? [String] {
return peers.count
}
}
return 0
case 2:
if UIDevice.current.hasCellularCapabilites {
return 3
}
return 2
default: return 0
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch indexPath.section {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: "discoveredPeerPrototype", for: indexPath)
let peers = app.yggdrasilSwitchPeers.sorted { (a, b) -> Bool in
return (a["Port"] as! Int) < (b["Port"] as! Int)
}
if indexPath.row < peers.count {
let value = peers[indexPath.row]
let proto = value["Protocol"] as? String ?? "tcp"
let sent = value["BytesSent"] as? Double ?? 0
let recvd = value["BytesRecvd"] as? Double ?? 0
let rx = self.format(bytes: sent)
let tx = self.format(bytes: recvd)
cell.textLabel?.text = "\(value["Endpoint"] ?? "unknown")"
cell.detailTextLabel?.text = "\(proto.uppercased()) peer on port \(value["Port"] ?? "unknown"), sent \(tx), received \(rx)"
}
return cell
case 1:
let cell = tableView.dequeueReusableCell(withIdentifier: "configuredPeerPrototype", for: indexPath)
if let config = self.app.yggdrasilConfig {
if let peers = config.get("Peers") as? [String] {
cell.textLabel?.text = peers[indexPath.last!]
} else {
cell.textLabel?.text = "(unknown)"
}
}
return cell
case 2:
switch indexPath.last {
case 0:
let cell = tableView.dequeueReusableCell(withIdentifier: "togglePrototype", for: indexPath) as! ToggleTableViewCell
cell.isUserInteractionEnabled = true
cell.label?.text = "Search for multicast peers"
cell.label?.isEnabled = true
cell.toggle?.addTarget(self, action: #selector(toggledMulticast), for: .valueChanged)
cell.toggle?.isEnabled = true
if let config = self.app.yggdrasilConfig {
let interfaces = config.get("MulticastInterfaces") as? [String] ?? []
cell.toggle?.isOn = interfaces.contains("en*")
}
return cell
case 1:
let cell = tableView.dequeueReusableCell(withIdentifier: "togglePrototype", for: indexPath) as! ToggleTableViewCell
cell.isUserInteractionEnabled = false
cell.label?.text = "Search for nearby iOS peers"
cell.label?.isEnabled = false
cell.toggle?.addTarget(self, action: #selector(toggledAWDL), for: .valueChanged)
cell.toggle?.setOn(false, animated: false)
cell.toggle?.isEnabled = false
/*if let config = self.app.yggdrasilConfig {
let interfaces = config.get("MulticastInterfaces") as? [String] ?? []
cell.toggle?.isOn = interfaces.contains("awdl0")
}*/
return cell
case 2:
let cell = tableView.dequeueReusableCell(withIdentifier: "menuPrototype", for: indexPath)
cell.isUserInteractionEnabled = true
cell.textLabel?.text = "Device settings"
cell.textLabel?.isEnabled = true
return cell
default:
let cell = tableView.dequeueReusableCell(withIdentifier: "menuPrototype", for: indexPath)
cell.isUserInteractionEnabled = false
cell.textLabel?.text = "Unknown"
cell.textLabel?.isEnabled = true
return cell
}
default:
let cell = tableView.dequeueReusableCell(withIdentifier: "configuredPeerPrototype", for: indexPath)
cell.textLabel?.text = "(unknown)"
return cell
}
}
func format(bytes: Double) -> String {
guard bytes > 0 else {
return "0 bytes"
}
// Adapted from http://stackoverflow.com/a/18650828
let suffixes = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
let k: Double = 1000
let i = floor(log(bytes) / log(k))
// Format number with thousands separator and everything below 1 GB with no decimal places.
let numberFormatter = NumberFormatter()
numberFormatter.maximumFractionDigits = i < 3 ? 0 : 1
numberFormatter.numberStyle = .decimal
let numberString = numberFormatter.string(from: NSNumber(value: bytes / pow(k, i))) ?? "Unknown"
let suffix = suffixes[Int(i)]
return "\(numberString) \(suffix)"
}
@objc func toggledMulticast(_ sender: UISwitch) {
if let config = self.app.yggdrasilConfig {
var interfaces = config.get("MulticastInterfaces") as! [String]
if sender.isOn {
interfaces.append("en*")
} else {
interfaces.removeAll(where: { $0 == "en*" })
}
config.set("MulticastInterfaces", to: interfaces as [String])
try? config.save(to: &app.vpnManager)
}
}
@objc func toggledAWDL(_ sender: UISwitch) {
if let config = self.app.yggdrasilConfig {
var interfaces = config.get("MulticastInterfaces") as! [String]
if sender.isOn {
interfaces.append("awdl0")
} else {
interfaces.removeAll(where: { $0 == "awdl0" })
}
config.set("MulticastInterfaces", to: interfaces as [String])
try? config.save(to: &app.vpnManager)
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0:
if self.app.yggdrasilPeers.count > 0 {
return "Connected Peers"
}
return "No peers currently connected"
case 1:
if let config = self.app.yggdrasilConfig {
if let peers = config.get("Peers") as? [String] {
if peers.count > 0 {
return "Configured Peers"
}
}
}
return "No peers currently configured"
case 2:
return "Peer Connectivity"
default: return "(Unknown)"
}
}
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
switch section {
case 1:
return "Yggdrasil will automatically attempt to connect to configured peers when started."
case 2:
var str = "Multicast peers will be discovered on the same Wi-Fi network or via USB."
if UIDevice.current.hasCellularCapabilites {
str += " Data charges may apply when using mobile data. You can prevent mobile data usage in the device settings."
}
return str
default: return nil
}
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return indexPath.first == 1
}
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
switch indexPath.first {
case 0:
return [UITableViewRowAction(style: UITableViewRowAction.Style.default, title: "Disconnect", handler: { (action, index) in
})]
case 1:
return [UITableViewRowAction(style: UITableViewRowAction.Style.normal, title: "Remove", handler: { (action, index) in
print(action, index)
if let config = self.app.yggdrasilConfig {
config.remove(index: index.last!, from: "Peers")
do {
try config.save(to: &self.app.vpnManager)
tableView.reloadSections(IndexSet(integer: 1), with: UITableView.RowAnimation.automatic)
} catch {
let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.parent?.present(alert, animated: true, completion: nil)
print("Error removing: \(error)")
}
}
})]
default:
return []
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch indexPath.first {
case 2:
if let last = indexPath.last, last == 2 {
UIApplication.shared.open(NSURL(string:UIApplication.openSettingsURLString)! as URL, options: [:]) { (result) in
NSLog("Result " + result.description)
}
}
default:
break
}
tableView.deselectRow(at: indexPath, animated: true)
}
@IBAction func addNewPeerButtonPressed(_ sender: UIBarButtonItem) {
let alert = UIAlertController(title: "Add Configured Peer", message: """
Enter the full URI of the peer to add. Yggdrasil will automatically connect to this peer when started.
""", preferredStyle: UIAlertController.Style.alert)
let action = UIAlertAction(title: "Add", style: .default) { (alertAction) in
let textField = alert.textFields![0] as UITextField
if let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) {
if let config = self.app.yggdrasilConfig {
if let peers = config.get("Peers") as? [String], !peers.contains(text) {
config.add(text, in: "Peers")
do {
try config.save(to: &self.app.vpnManager)
if let index = config.get("Peers") as? [String] {
self.peerTable.insertRows(at: [IndexPath(indexes: [1, index.count-1])], with: .automatic)
self.peerTable.reloadSections(IndexSet(integer: 1), with: UITableView.RowAnimation.automatic)
}
} catch {
let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.parent?.present(alert, animated: true, completion: nil)
print("Add error: \(error)")
}
} else {
let alert = UIAlertController(title: "Error", message: "Peer already exists", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
self.parent?.present(alert, animated: true, completion: nil)
}
}
}
}
let cancel = UIAlertAction(title: "Cancel", style: .cancel)
alert.addTextField { (textField) in
textField.placeholder = "tcp://hostname:port"
}
alert.addAction(action)
alert.addAction(cancel)
self.present(alert, animated: true, completion: nil)
}
}