Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Siri Shortcuts integration for iOS #8

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a0fe44d
Automatic configuration files changes by Xcode
alessionossa Jan 28, 2022
3c63c38
project: Added WireGuardIntentsExtensioniOS target and Siri capability
alessionossa Jan 28, 2022
0dd0672
project: Added sources to WireGuardIntentsExtensioniOS
alessionossa Jan 29, 2022
ab1e96f
Implemented GetPeers intent
alessionossa Jan 29, 2022
ceabb4e
WireguardApp: iOS: Moved tunnelsManager initialization to AppDelegate
alessionossa Jan 29, 2022
fe3f2d0
Implemented UpdateConfiguration intent
alessionossa Feb 1, 2022
bb6ea1b
Implemented SetTunnelStatus intent
alessionossa Feb 1, 2022
c29787f
WireguardApp: iOS: Added Siri shortcuts donations for SetTunnelStatus…
alessionossa Feb 1, 2022
63c0956
Removed SetTunnelStatus
alessionossa Apr 1, 2023
7ec6974
Complete cleanup after SetTunnelStatus Intent removal
alessionossa Apr 1, 2023
2952206
Remove GetPeers SiriKit Intent
alessionossa Apr 5, 2023
bee5d34
Implement GetPeers AppIntent
alessionossa Apr 11, 2023
d05a169
Removed UpdateConfigurationIntent SiriKit Intent
alessionossa Apr 5, 2023
c1d7199
Remove WireGuardIntentsExtension
alessionossa Apr 5, 2023
a0a6f26
WireguardApp: iOS: Cleanup after SiriKit Intents removal
alessionossa Apr 11, 2023
1cb0653
WireguardApp: Add async variant of modify tunnel function
alessionossa Apr 11, 2023
ba250fe
Implement UpdateConfiguration AppIntent with Dictionary as input
alessionossa Apr 11, 2023
eec11c1
Remove UpdateConfiguration Intent with Dictionary input
alessionossa Apr 11, 2023
e13bc40
Implement BuildPeerConfigurationUpdate App Intent
alessionossa Apr 11, 2023
ee03536
Implement UpdateTunnelConfiguration App Intent
alessionossa Apr 11, 2023
97bf1c1
WireguardApp: macOS: Add App Intents to macOS app
alessionossa Apr 11, 2023
7abdf6e
Fix return value of AppIntents in iOS 17
alessionossa Feb 27, 2024
56c4d6c
Remove missing peers error in UpdateTunnelConfiguration Intent
alessionossa Feb 27, 2024
7da272a
Rename constant to satisfy SwiftLint
alessionossa Feb 27, 2024
06ed6e9
Update AppIntents Strings
alessionossa Feb 27, 2024
8a2a4eb
Add backwards compatibility of GetPeers wit SiriKit version
alessionossa Feb 27, 2024
2f99f5c
Remove backward compatibility of GetPeers with SiriKit version
alessionossa Feb 27, 2024
a37ae8a
Merge branch 'master' into an/shortcuts-integration
alessionossa Feb 28, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
719 changes: 719 additions & 0 deletions Sources/Shared/Intents.intentdefinition

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions Sources/WireGuardApp/Tunnel/TunnelsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import Foundation
import NetworkExtension
import os.log

#if os(iOS)
import Intents
#endif

protocol TunnelsManagerListDelegate: AnyObject {
func tunnelAdded(at index: Int)
func tunnelModified(at index: Int)
Expand Down Expand Up @@ -307,6 +311,12 @@ class TunnelsManager {
}
#elseif os(iOS)
(tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()

INInteraction.delete(with: "com.wireguard.intents.tunnel.\(tunnel.name)") { error in
if let error = error {
wg_log(.error, message: "Error deleting donated interactions for tunnel \(tunnel.name): \(error.localizedDescription)")
}
}
#else
#error("Unimplemented")
#endif
Expand Down
113 changes: 113 additions & 0 deletions Sources/WireGuardApp/UI/iOS/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import UIKit
import os.log
import Intents

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
Expand All @@ -11,6 +12,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var mainVC: MainViewController?
var isLaunchedForSpecificAction = false

var tunnelsManager: TunnelsManager?

static let tunnelsManagerReadyNotificationName: Notification.Name = Notification.Name(rawValue: "com.wireguard.ios.tunnelsManagerReadyNotification")

func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Logger.configureGlobal(tagged: "APP", withFilePath: FileManager.logFileURL?.path)

Expand All @@ -29,6 +34,25 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

self.mainVC = mainVC

// Create the tunnels manager, and when it's ready, inform tunnelsListVC
TunnelsManager.create { [weak self] result in
guard let self = self else { return }

switch result {
case .failure(let error):
ErrorPresenter.showErrorAlert(error: error, from: self.mainVC)
case .success(let tunnelsManager):
self.tunnelsManager = tunnelsManager
self.mainVC?.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)

tunnelsManager.activationDelegate = self.mainVC

NotificationCenter.default.post(name: AppDelegate.tunnelsManagerReadyNotificationName,
object: self,
userInfo: nil)
}
}

return true
}

Expand Down Expand Up @@ -82,3 +106,92 @@ extension AppDelegate {
return nil
}
}

extension AppDelegate {

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {

guard let interaction = userActivity.interaction else {
return false
}

if interaction.intent is UpdateConfigurationIntent {
if let tunnelsManager = tunnelsManager {
self.handleupdateConfigurationIntent(interaction: interaction, tunnelsManager: tunnelsManager)
} else {
var token: NSObjectProtocol?
token = NotificationCenter.default.addObserver(forName: AppDelegate.tunnelsManagerReadyNotificationName, object: nil, queue: .main) { [weak self] _ in
guard let tunnelsManager = self?.tunnelsManager else { return }

self?.handleupdateConfigurationIntent(interaction: interaction, tunnelsManager: tunnelsManager)
NotificationCenter.default.removeObserver(token!)
}
}

return true
}

return false
}

func handleupdateConfigurationIntent(interaction: INInteraction, tunnelsManager: TunnelsManager) {

guard let updateConfigurationIntent = interaction.intent as? UpdateConfigurationIntent,
let configurationUpdates = interaction.intentResponse?.userActivity?.userInfo else {
return
}

guard let tunnelName = updateConfigurationIntent.tunnel,
let configurations = configurationUpdates["Configuration"] as? [String: [String: String]] else {
wg_log(.error, message: "Failed to get informations to update the configuration")
return
}

guard let tunnel = tunnelsManager.tunnel(named: tunnelName),
let tunnelConfiguration = tunnel.tunnelConfiguration else {
wg_log(.error, message: "Failed to get tunnel configuration with name \(tunnelName)")
ErrorPresenter.showErrorAlert(title: "Tunnel not found",
message: "Tunnel with name '\(tunnelName)' is not present.",
from: self.mainVC)
return
}

var peers = tunnelConfiguration.peers

for (peerPubKey, valuesToUpdate) in configurations {
guard let peerIndex = peers.firstIndex(where: { $0.publicKey.base64Key == peerPubKey }) else {
wg_log(.debug, message: "Failed to find peer \(peerPubKey) in tunnel with name \(tunnelName)")
ErrorPresenter.showErrorAlert(title: "Peer not found",
message: "Peer '\(peerPubKey)' is not present in '\(tunnelName)' tunnel.",
from: self.mainVC)
continue
}

if let endpointString = valuesToUpdate["Endpoint"] {
if let newEntpoint = Endpoint(from: endpointString) {
peers[peerIndex].endpoint = newEntpoint
} else {
wg_log(.debug, message: "Failed to convert \(endpointString) to Endpoint")
}
}
}

let newConfiguration = TunnelConfiguration(name: tunnel.name, interface: tunnelConfiguration.interface, peers: peers)

tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: newConfiguration, onDemandOption: tunnel.onDemandOption) { error in
guard error == nil else {
wg_log(.error, message: error!.localizedDescription)
ErrorPresenter.showErrorAlert(error: error!, from: self.mainVC)
return
}

if let completionUrlString = updateConfigurationIntent.completionUrl,
!completionUrlString.isEmpty,
let completionUrl = URL(string: completionUrlString) {
UIApplication.shared.open(completionUrl, options: [:], completionHandler: nil)
}

wg_log(.debug, message: "Updated configuration of tunnel \(tunnelName)")
}
}
}
19 changes: 11 additions & 8 deletions Sources/WireGuardApp/UI/iOS/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
Expand Down Expand Up @@ -64,8 +64,6 @@
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDisplayName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
Expand All @@ -74,17 +72,24 @@
<string>$(VERSION_NAME)</string>
<key>CFBundleVersion</key>
<string>$(VERSION_ID)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<false/>
<key>NSCameraUsageDescription</key>
<string>Localized</string>
<key>NSFaceIDUsageDescription</key>
<string>Localized</string>
<key>NSUserActivityTypes</key>
<array>
<string>UpdateConfigurationIntent</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
</array>
<array/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
Expand Down Expand Up @@ -123,8 +128,6 @@
</dict>
</dict>
</array>
<key>NSFaceIDUsageDescription</key>
<string>Localized</string>
<key>com.wireguard.ios.app_group_id</key>
<string>group.$(APP_ID_IOS)</string>
</dict>
Expand Down
49 changes: 31 additions & 18 deletions Sources/WireGuardApp/UI/iOS/ViewController/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.

import UIKit
import Intents

class MainViewController: UISplitViewController {

var tunnelsManager: TunnelsManager?
var tunnelsManager: TunnelsManager? {
return (UIApplication.shared.delegate as? AppDelegate)?.tunnelsManager
}
var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
var tunnelsListVC: TunnelsListTableViewController?

Expand Down Expand Up @@ -42,29 +45,24 @@ class MainViewController: UISplitViewController {
// On iPad, always show both masterVC and detailVC, even in portrait mode, like the Settings app
preferredDisplayMode = .allVisible

// Create the tunnels manager, and when it's ready, inform tunnelsListVC
TunnelsManager.create { [weak self] result in
guard let self = self else { return }

switch result {
case .failure(let error):
ErrorPresenter.showErrorAlert(error: error, from: self)
case .success(let tunnelsManager):
self.tunnelsManager = tunnelsManager
self.tunnelsListVC?.setTunnelsManager(tunnelsManager: tunnelsManager)

tunnelsManager.activationDelegate = self

self.onTunnelsManagerReady?(tunnelsManager)
self.onTunnelsManagerReady = nil
}
}
NotificationCenter.default.addObserver(self, selector: #selector(handleTunnelsManagerReady(_:)),
name: AppDelegate.tunnelsManagerReadyNotificationName, object: nil)
}

func allTunnelNames() -> [String]? {
guard let tunnelsManager = self.tunnelsManager else { return nil }
return tunnelsManager.mapTunnels { $0.name }
}

@objc
func handleTunnelsManagerReady(_ notification: Notification) {
guard let tunnelsManager = self.tunnelsManager else { return }

self.onTunnelsManagerReady?(tunnelsManager)
self.onTunnelsManagerReady = nil

NotificationCenter.default.removeObserver(self, name: AppDelegate.tunnelsManagerReadyNotificationName, object: nil)
}
}

extension MainViewController: TunnelsManagerActivationDelegate {
Expand Down Expand Up @@ -99,10 +97,25 @@ extension MainViewController {
if let tunnel = tunnelsManager.tunnel(named: tunnelName) {
tunnelsListVC.showTunnelDetail(for: tunnel, animated: false)
if shouldToggleStatus {

let intent = SetTunnelStatusIntent()
intent.tunnel = tunnel.name
intent.operation = .turn

if tunnel.status == .inactive {
tunnelsManager.startActivation(of: tunnel)
intent.state = .on
} else if tunnel.status == .active {
tunnelsManager.startDeactivation(of: tunnel)
intent.state = .off
}

let interaction = INInteraction(intent: intent, response: nil)
interaction.groupIdentifier = "com.wireguard.intents.tunnel.\(tunnel.name)"
interaction.donate { error in
if let error = error {
wg_log(.error, message: "Error donating interaction for SetTunnelStatusIntent: \(error.localizedDescription)")
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Copyright © 2018-2021 WireGuard LLC. All Rights Reserved.

import UIKit
import Intents

class TunnelDetailTableViewController: UITableViewController {

Expand Down Expand Up @@ -387,6 +388,18 @@ extension TunnelDetailTableViewController {
cell.onSwitchToggled = { [weak self] isOn in
guard let self = self else { return }

let intent = SetTunnelStatusIntent()
intent.tunnel = self.tunnel.name
intent.operation = .turn
intent.state = isOn ? .on : .off
let interaction = INInteraction(intent: intent, response: nil)
interaction.groupIdentifier = "com.wireguard.intents.tunnel.\(self.tunnel.name)"
interaction.donate { error in
if let error = error {
wg_log(.error, message: "Error donating interaction for SetTunnelStatusIntent: \(error.localizedDescription)")
}
}

if self.tunnel.hasOnDemandRules {
self.tunnelsManager.setOnDemandEnabled(isOn, on: self.tunnel) { error in
if error == nil && !isOn {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import UIKit
import MobileCoreServices
import UserNotifications
import Intents

class TunnelsListTableViewController: UIViewController {

Expand Down Expand Up @@ -317,6 +318,19 @@ extension TunnelsListTableViewController: UITableViewDataSource {
cell.tunnel = tunnel
cell.onSwitchToggled = { [weak self] isOn in
guard let self = self, let tunnelsManager = self.tunnelsManager else { return }

let intent = SetTunnelStatusIntent()
intent.tunnel = tunnel.name
intent.operation = .turn
intent.state = isOn ? .on : .off
let interaction = INInteraction(intent: intent, response: nil)
interaction.groupIdentifier = "com.wireguard.intents.tunnel.\(tunnel.name)"
interaction.donate { error in
if let error = error {
wg_log(.error, message: "Error donating interaction for SetTunnelStatusIntent: \(error.localizedDescription)")
}
}

if tunnel.hasOnDemandRules {
tunnelsManager.setOnDemandEnabled(isOn, on: tunnel) { error in
if error == nil && !isOn {
Expand Down
2 changes: 2 additions & 0 deletions Sources/WireGuardApp/UI/iOS/WireGuard.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
</array>
<key>com.apple.developer.networking.wifi-info</key>
<true/>
<key>com.apple.developer.siri</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.$(APP_ID_IOS)</string>
Expand Down
Loading