diff options
author | Eric Kuck <eric@bluelinelabs.com> | 2018-08-21 11:00:41 -0500 |
---|---|---|
committer | Eric Kuck <eric@bluelinelabs.com> | 2018-08-21 11:00:41 -0500 |
commit | 39ae9db11c3b243b8171301e31f690b9ba3941ae (patch) | |
tree | 4d467381c08de38de7c749c306e5dbe8f19d124c | |
parent | Use first peer's endpoint as the tunnel remote address. (diff) | |
download | wireguard-apple-39ae9db11c3b243b8171301e31f690b9ba3941ae.tar.xz wireguard-apple-39ae9db11c3b243b8171301e31f690b9ba3941ae.zip |
Added ability to add tunnels with a QR code scan. Logic in place to parse conf files as well.
Signed-off-by: Eric Kuck <eric@bluelinelabs.com>
-rw-r--r-- | WireGuard.xcodeproj/project.pbxproj | 8 | ||||
-rw-r--r-- | WireGuard/Base.lproj/Main.storyboard | 37 | ||||
-rw-r--r-- | WireGuard/Coordinators/AppCoordinator.swift | 41 | ||||
-rw-r--r-- | WireGuard/Info.plist | 2 | ||||
-rw-r--r-- | WireGuard/Models/Attribute.swift | 44 | ||||
-rw-r--r-- | WireGuard/Models/Interface+Extension.swift | 21 | ||||
-rw-r--r-- | WireGuard/Models/Peer+Extension.swift | 19 | ||||
-rw-r--r-- | WireGuard/Models/Tunnel+Extension.swift | 53 | ||||
-rw-r--r-- | WireGuard/ViewControllers/QRScanViewController.swift | 107 | ||||
-rw-r--r-- | WireGuard/ViewControllers/TunnelConfigurationTableViewController.swift | 1 |
10 files changed, 325 insertions, 8 deletions
diff --git a/WireGuard.xcodeproj/project.pbxproj b/WireGuard.xcodeproj/project.pbxproj index 620f25c..e731b4e 100644 --- a/WireGuard.xcodeproj/project.pbxproj +++ b/WireGuard.xcodeproj/project.pbxproj @@ -46,6 +46,8 @@ 5FA1D4CD2124A05C00DBA2E6 /* Interface+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FA1D4CC2124A05C00DBA2E6 /* Interface+Extension.swift */; }; 5FA1D5102124D80C00DBA2E6 /* String+Arrays.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FA1D50F2124D80C00DBA2E6 /* String+Arrays.swift */; }; 5FA1D5122124DA6400DBA2E6 /* String+Base64.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FA1D5112124DA6400DBA2E6 /* String+Base64.swift */; }; + 5FCC4343212B3092009A9C58 /* QRScanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCC4342212B3092009A9C58 /* QRScanViewController.swift */; }; + 5FCC4347212B3E2C009A9C58 /* Attribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCC4346212B3E2C009A9C58 /* Attribute.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -126,6 +128,8 @@ 5FA1D4CC2124A05C00DBA2E6 /* Interface+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Interface+Extension.swift"; sourceTree = "<group>"; }; 5FA1D50F2124D80C00DBA2E6 /* String+Arrays.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Arrays.swift"; sourceTree = "<group>"; }; 5FA1D5112124DA6400DBA2E6 /* String+Base64.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Base64.swift"; sourceTree = "<group>"; }; + 5FCC4342212B3092009A9C58 /* QRScanViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScanViewController.swift; sourceTree = "<group>"; }; + 5FCC4346212B3E2C009A9C58 /* Attribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attribute.swift; sourceTree = "<group>"; }; 861983CAE8FDC13BC83E7E04 /* Pods_WireGuard.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WireGuard.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -235,6 +239,7 @@ 4A8AABD720B6A79100B6D8C1 /* UITableView+WireGuard.swift */, 4A4BACE720B5F1BF00F12B28 /* TunnelsTableViewController.swift */, 4A4BA6D720B73CBA00223AB8 /* TunnelConfigurationTableViewController.swift */, + 5FCC4342212B3092009A9C58 /* QRScanViewController.swift */, ); path = ViewControllers; sourceTree = "<group>"; @@ -252,6 +257,7 @@ 4A4BAD1920B5F8FF00F12B28 /* Tunnel+CoreDataProperties.swift */, 4AC5462D2116306F00749D21 /* Tunnel+Extension.swift */, 4A4BAD1520B5F8DE00F12B28 /* WireGuard.xcdatamodeld */, + 5FCC4346212B3E2C009A9C58 /* Attribute.swift */, ); path = Models; sourceTree = "<group>"; @@ -550,6 +556,7 @@ 4AEAC32B20F14BA9007B67AB /* Log.swift in Sources */, 4A4BAD1320B5F82400F12B28 /* Identifyable.swift in Sources */, 4A4BAD1720B5F8DE00F12B28 /* WireGuard.xcdatamodeld in Sources */, + 5FCC4347212B3E2C009A9C58 /* Attribute.swift in Sources */, 4A4BAD1A20B5F8FF00F12B28 /* Tunnel+CoreDataClass.swift in Sources */, 4A4BACE820B5F1BF00F12B28 /* TunnelsTableViewController.swift in Sources */, 4A4BAD1020B5F6EC00F12B28 /* RootCoordinator.swift in Sources */, @@ -557,6 +564,7 @@ 5FA1D4CB21249F7D00DBA2E6 /* Peer+Extension.swift in Sources */, 5FA1D5122124DA6400DBA2E6 /* String+Base64.swift in Sources */, 4AC5462E2116306F00749D21 /* Tunnel+Extension.swift in Sources */, + 5FCC4343212B3092009A9C58 /* QRScanViewController.swift in Sources */, 4A4BAD0E20B5F6C300F12B28 /* Coordinator.swift in Sources */, 4A4BA6D820B73CBA00223AB8 /* TunnelConfigurationTableViewController.swift in Sources */, 4A4BAD2020B6026900F12B28 /* Peer+CoreDataProperties.swift in Sources */, diff --git a/WireGuard/Base.lproj/Main.storyboard b/WireGuard/Base.lproj/Main.storyboard index 49fa373..80639f1 100644 --- a/WireGuard/Base.lproj/Main.storyboard +++ b/WireGuard/Base.lproj/Main.storyboard @@ -6,6 +6,7 @@ <dependencies> <deployment identifier="iOS"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.9"/> + <capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> </dependencies> <scenes> @@ -73,12 +74,12 @@ </tableViewController> <placeholder placeholderIdentifier="IBFirstResponder" id="4uZ-Vv-Fry" userLabel="First Responder" sceneMemberID="firstResponder"/> </objects> - <point key="canvasLocation" x="34" y="154"/> + <point key="canvasLocation" x="-670" y="200"/> </scene> <!--Tunnel settings--> <scene sceneID="xV8-BW-4R7"> <objects> - <tableViewController storyboardIdentifier="TunnelConfigurationTableViewController" id="0VM-73-EPX" customClass="TunnelConfigurationTableViewController" customModule="WireGuard" customModuleProvider="target" sceneMemberID="viewController"> + <tableViewController storyboardIdentifier="TunnelConfigurationTableViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="0VM-73-EPX" customClass="TunnelConfigurationTableViewController" customModule="WireGuard" customModuleProvider="target" sceneMemberID="viewController"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="0Uy-k2-O3i"> <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> @@ -472,7 +473,37 @@ </tableViewController> <placeholder placeholderIdentifier="IBFirstResponder" id="j96-PK-ghN" userLabel="First Responder" sceneMemberID="firstResponder"/> </objects> - <point key="canvasLocation" x="1128.8" y="321.58920539730138"/> + <point key="canvasLocation" x="122" y="-239"/> + </scene> + <!--Scan Code--> + <scene sceneID="gKN-k2-HoW"> + <objects> + <viewController storyboardIdentifier="QRScanViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="Efe-yN-iDH" customClass="QRScanViewController" customModule="WireGuard" customModuleProvider="target" sceneMemberID="viewController"> + <view key="view" contentMode="scaleToFill" id="NXo-On-ea8"> + <rect key="frame" x="0.0" y="0.0" width="375" height="667"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Tip: generate with `qrencode -t ansiutf8 < tunnel.conf`" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="XYc-tx-YNF"> + <rect key="frame" x="16" y="628.5" width="343" height="14.5"/> + <fontDescription key="fontDescription" type="system" pointSize="12"/> + <color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <constraints> + <constraint firstItem="Soo-c9-MsX" firstAttribute="bottom" secondItem="XYc-tx-YNF" secondAttribute="bottom" constant="24" id="QhS-p5-jbw"/> + <constraint firstItem="XYc-tx-YNF" firstAttribute="leading" secondItem="NXo-On-ea8" secondAttribute="leading" constant="16" id="Y3P-Py-ueV"/> + <constraint firstAttribute="trailing" secondItem="XYc-tx-YNF" secondAttribute="trailing" constant="16" id="sB1-h9-ueh"/> + </constraints> + <viewLayoutGuide key="safeArea" id="Soo-c9-MsX"/> + </view> + <navigationItem key="navigationItem" title="Scan Code" largeTitleDisplayMode="never" id="WGY-tY-ySz"/> + <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="TQ2-zp-o40" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="121" y="461"/> </scene> </scenes> <resources> diff --git a/WireGuard/Coordinators/AppCoordinator.swift b/WireGuard/Coordinators/AppCoordinator.swift index 75657f0..f57a9d5 100644 --- a/WireGuard/Coordinators/AppCoordinator.swift +++ b/WireGuard/Coordinators/AppCoordinator.swift @@ -32,7 +32,6 @@ class AppCoordinator: RootViewCoordinator { return self.tunnelsTableViewController } - var tunnelsTableViewController: TunnelsTableViewController! /// Window to manage @@ -134,10 +133,33 @@ class AppCoordinator: RootViewCoordinator { extension AppCoordinator: TunnelsTableViewControllerDelegate { func addProvider(tunnelsTableViewController: TunnelsTableViewController) { + let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + actionSheet.addAction(UIAlertAction(title: "Add Manually", style: .default) { [unowned self] _ in + self.addProviderManually() + }) + actionSheet.addAction(UIAlertAction(title: "Scan QR Code", style: .default) { [unowned self] _ in + self.addProviderWithQRScan() + }) + actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + + tunnelsTableViewController.present(actionSheet, animated: true, completion: nil) + } + + func addProviderManually() { let addContext = persistentContainer.newBackgroundContext() showTunnelConfigurationViewController(tunnel: nil, context: addContext) } + func addProviderWithQRScan() { + let addContext = persistentContainer.newBackgroundContext() + + let qrScanViewController = storyboard.instantiateViewController(type: QRScanViewController.self) + + qrScanViewController.configure(context: addContext, delegate: self) + + self.navigationController.pushViewController(qrScanViewController, animated: true) + } + func connect(tunnel: Tunnel, tunnelsTableViewController: TunnelsTableViewController) { let manager = self.providerManager(for: tunnel)! let block = { @@ -237,10 +259,8 @@ extension AppCoordinator: TunnelsTableViewControllerDelegate { return tunnelIdentifier == tunnel.tunnelIdentifier } } -} -extension AppCoordinator: TunnelConfigurationTableViewControllerDelegate { - func didSave(tunnel: Tunnel, tunnelConfigurationTableViewController: TunnelConfigurationTableViewController) { + private func saveTunnel(_ tunnel: Tunnel) { let manager = providerManager(for: tunnel) ?? NETunnelProviderManager() manager.localizedDescription = tunnel.title @@ -265,5 +285,18 @@ extension AppCoordinator: TunnelConfigurationTableViewControllerDelegate { navigationController.popToRootViewController(animated: true) } +} + +extension AppCoordinator: TunnelConfigurationTableViewControllerDelegate { + func didSave(tunnel: Tunnel, tunnelConfigurationTableViewController: TunnelConfigurationTableViewController) { + saveTunnel(tunnel) + } + +} + +extension AppCoordinator: QRScanViewControllerDelegate { + func didSave(tunnel: Tunnel, qrScanViewController: QRScanViewController) { + showTunnelConfigurationViewController(tunnel: tunnel, context: tunnel.managedObjectContext!) + } } diff --git a/WireGuard/Info.plist b/WireGuard/Info.plist index 685b9e2..f3073e3 100644 --- a/WireGuard/Info.plist +++ b/WireGuard/Info.plist @@ -2,6 +2,8 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> + <key>NSCameraUsageDescription</key> + <string>Camera is used to scan QR codes</string> <key>CFBundleDevelopmentRegion</key> <string>$(DEVELOPMENT_LANGUAGE)</string> <key>CFBundleExecutable</key> diff --git a/WireGuard/Models/Attribute.swift b/WireGuard/Models/Attribute.swift new file mode 100644 index 0000000..51f7864 --- /dev/null +++ b/WireGuard/Models/Attribute.swift @@ -0,0 +1,44 @@ +// +// Attribute.swift +// WireGuard +// +// Created by Eric Kuck on 8/20/18. +// Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All rights reserved. +// + +import Foundation + +struct Attribute { + + enum Key: String, CaseIterable { + case address = "Address" + case allowedIPs = "AllowedIPs" + case dns = "DNS" + case endpoint = "Endpoint" + case listenPort = "ListenPort" + case mtu = "MTU" + case persistentKeepalive = "PersistentKeepalive" + case presharedKey = "PresharedKey" + case privateKey = "PrivateKey" + case publicKey = "PublicKey" + } + + private static let separatorPattern = (try? NSRegularExpression(pattern: "\\s|=", options: []))! + + let line: String + let key: Key + let stringValue: String + var arrayValue: [String] { + return stringValue.commaSeparatedToArray() + } + + static func match(line: String) -> Attribute? { + guard let equalsIndex = line.firstIndex(of: "=") else { return nil } + let keyString = line[..<equalsIndex].trimmingCharacters(in: .whitespaces) + let value = line[line.index(equalsIndex, offsetBy: 1)...].trimmingCharacters(in: .whitespaces) + guard let key = Key.allCases.first(where: { $0.rawValue.lowercased() == keyString.lowercased() }) else { return nil } + + return Attribute(line: line, key: key, stringValue: value) + } + +} diff --git a/WireGuard/Models/Interface+Extension.swift b/WireGuard/Models/Interface+Extension.swift index 78ef567..060b4b7 100644 --- a/WireGuard/Models/Interface+Extension.swift +++ b/WireGuard/Models/Interface+Extension.swift @@ -36,6 +36,27 @@ extension Interface { } } + func parse(attribute: Attribute) throws { + switch attribute.key { + case .address: + addresses = attribute.stringValue + case .dns: + dns = attribute.stringValue + case .listenPort: + if let port = Int16(attribute.stringValue) { + listenPort = port + } + case .mtu: + if let mtu = Int32(attribute.stringValue) { + self.mtu = mtu + } + case .privateKey: + privateKey = attribute.stringValue + default: + throw TunnelParseError.invalidLine(attribute.line) + } + } + } enum InterfaceValidationError: Error { diff --git a/WireGuard/Models/Peer+Extension.swift b/WireGuard/Models/Peer+Extension.swift index c27748c..f83c038 100644 --- a/WireGuard/Models/Peer+Extension.swift +++ b/WireGuard/Models/Peer+Extension.swift @@ -44,6 +44,25 @@ extension Peer { } } + func parse(attribute: Attribute) throws { + switch attribute.key { + case .allowedIPs: + allowedIPs = attribute.stringValue + case .endpoint: + endpoint = attribute.stringValue + case .persistentKeepalive: + if let keepAlive = Int32(attribute.stringValue) { + persistentKeepalive = keepAlive + } + case .presharedKey: + presharedKey = attribute.stringValue + case .publicKey: + publicKey = attribute.stringValue + default: + throw TunnelParseError.invalidLine(attribute.line) + } + } + } enum PeerValidationError: Error { diff --git a/WireGuard/Models/Tunnel+Extension.swift b/WireGuard/Models/Tunnel+Extension.swift index 038e47d..62014f1 100644 --- a/WireGuard/Models/Tunnel+Extension.swift +++ b/WireGuard/Models/Tunnel+Extension.swift @@ -111,6 +111,54 @@ extension Tunnel { } } + static func fromConfig(_ text: String, context: NSManagedObjectContext) throws -> Tunnel { + let lines = text.split(separator: "\n") + + var currentPeer: Peer? + var isInInterfaceSection = false + + var tunnel: Tunnel! + context.performAndWait { + tunnel = Tunnel(context: context) + tunnel.interface = Interface(context: context) + } + tunnel.tunnelIdentifier = UUID().uuidString + + for line in lines { + var trimmedLine: String + if let commentRange = line.range(of: "#") { + trimmedLine = String(line[..<commentRange.lowerBound]) + } else { + trimmedLine = String(line) + } + + trimmedLine = trimmedLine.trimmingCharacters(in: .whitespaces) + + guard trimmedLine.count > 0 else { continue } + + if "[interface]" == line.lowercased() { + currentPeer = nil + isInInterfaceSection = true + } else if "[peer]" == line.lowercased() { + context.performAndWait { currentPeer = Peer(context: context) } + tunnel.insertIntoPeers(currentPeer!, at: tunnel.peers?.count ?? 0) + isInInterfaceSection = false + } else if isInInterfaceSection, let attribute = Attribute.match(line: String(line)) { + try tunnel.interface!.parse(attribute: attribute) + } else if let currentPeer = currentPeer, let attribute = Attribute.match(line: String(line)) { + try currentPeer.parse(attribute: attribute) + } else { + throw TunnelParseError.invalidLine(String(line)) + } + } + + if !isInInterfaceSection && currentPeer == nil { + throw TunnelParseError.noConfigInfo + } + + return tunnel + } + } private func base64KeyToHex(_ base64: String?) -> String? { @@ -146,3 +194,8 @@ enum TunnelValidationError: Error { case nilPeers case invalidPeer } + +enum TunnelParseError: Error { + case invalidLine(_ line: String) + case noConfigInfo +} diff --git a/WireGuard/ViewControllers/QRScanViewController.swift b/WireGuard/ViewControllers/QRScanViewController.swift new file mode 100644 index 0000000..f15d30b --- /dev/null +++ b/WireGuard/ViewControllers/QRScanViewController.swift @@ -0,0 +1,107 @@ +// +// QRScanViewController.swift +// WireGuard +// +// Created by Eric Kuck on 8/20/18. +// Copyright © 2018 Jason A. Donenfeld <Jason@zx2c4.com>. All rights reserved. +// + +import AVFoundation +import CoreData +import UIKit + +protocol QRScanViewControllerDelegate: class { + func didSave(tunnel: Tunnel, qrScanViewController: QRScanViewController) +} + +class QRScanViewController: UIViewController { + + private var viewContext: NSManagedObjectContext! + private weak var delegate: QRScanViewControllerDelegate? + var captureSession: AVCaptureSession? = AVCaptureSession() + let metadataOutput = AVCaptureMetadataOutput() + var previewLayer: AVCaptureVideoPreviewLayer! + + func configure(context: NSManagedObjectContext, delegate: QRScanViewControllerDelegate? = nil) { + viewContext = context + self.delegate = delegate + } + + override func viewDidLoad() { + super.viewDidLoad() + + guard let videoCaptureDevice = AVCaptureDevice.default(for: .video), + let videoInput = try? AVCaptureDeviceInput(device: videoCaptureDevice), + let captureSession = captureSession, + captureSession.canAddInput(videoInput), + captureSession.canAddOutput(metadataOutput) else { + scanDidEncounterError(title: "Scanning Not Supported", message: "This device does not have the ability to scan QR codes.") + return + } + + captureSession.addInput(videoInput) + captureSession.addOutput(metadataOutput) + + metadataOutput.setMetadataObjectsDelegate(self, queue: .main) + metadataOutput.metadataObjectTypes = [.qr] + + previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) + previewLayer.frame = view.layer.bounds + previewLayer.videoGravity = .resizeAspectFill + view.layer.addSublayer(previewLayer) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + if captureSession?.isRunning == false { + captureSession?.startRunning() + } + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + + if captureSession?.isRunning == true { + captureSession?.stopRunning() + } + } + + func scanDidComplete(withCode code: String) { + do { + let tunnel = try Tunnel.fromConfig(code, context: viewContext) + delegate?.didSave(tunnel: tunnel, qrScanViewController: self) + } catch { + scanDidEncounterError(title: "Invalid Code", message: "The scanned code is not a valid WireGuard config file.") + } + } + + func scanDidEncounterError(title: String, message: String) { + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + alertController.addAction(UIAlertAction(title: "OK", style: .default, handler: { [weak self] _ in + self?.navigationController?.popViewController(animated: true) + })) + present(alertController, animated: true) + captureSession = nil + } + +} + +extension QRScanViewController: AVCaptureMetadataOutputObjectsDelegate { + func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + captureSession?.stopRunning() + + guard let metadataObject = metadataObjects.first, + let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject, + let stringValue = readableObject.stringValue else { + scanDidEncounterError(title: "Invalid Code", message: "The scanned code could not be read.") + return + } + + AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) + scanDidComplete(withCode: stringValue) + } + +} + +extension QRScanViewController: Identifyable {} diff --git a/WireGuard/ViewControllers/TunnelConfigurationTableViewController.swift b/WireGuard/ViewControllers/TunnelConfigurationTableViewController.swift index 6159b00..8aff110 100644 --- a/WireGuard/ViewControllers/TunnelConfigurationTableViewController.swift +++ b/WireGuard/ViewControllers/TunnelConfigurationTableViewController.swift @@ -27,7 +27,6 @@ class TunnelConfigurationTableViewController: UITableViewController { viewContext = context self.delegate = delegate self.tunnel = tunnel ?? generateNewTunnelConfig() - } private func generateNewTunnelConfig() -> Tunnel { |