aboutsummaryrefslogtreecommitdiffstats
path: root/Sources/WireGuardApp
diff options
context:
space:
mode:
authorAndrej Mihajlov <and@mullvad.net>2020-12-02 12:27:39 +0100
committerAndrej Mihajlov <and@mullvad.net>2020-12-03 13:32:24 +0100
commitec574085703ea1c8b2d4538596961beb910c4382 (patch)
tree73cf8bbdb74fe5575606664bccd0232ffa911803 /Sources/WireGuardApp
parentWireGuardKit: Assert that resolutionResults must not contain failures (diff)
downloadwireguard-apple-ec574085703ea1c8b2d4538596961beb910c4382.tar.xz
wireguard-apple-ec574085703ea1c8b2d4538596961beb910c4382.zip
Move all source files to `Sources/` and rename WireGuardKit targets
Signed-off-by: Andrej Mihajlov <and@mullvad.net>
Diffstat (limited to 'Sources/WireGuardApp')
-rw-r--r--Sources/WireGuardApp/Base.lproj/InfoPlist.strings7
-rw-r--r--Sources/WireGuardApp/Base.lproj/Localizable.strings445
-rw-r--r--Sources/WireGuardApp/Config/Config.xcconfig2
-rw-r--r--Sources/WireGuardApp/Config/Developer.xcconfig.template10
-rw-r--r--Sources/WireGuardApp/Config/Version.xcconfig2
-rw-r--r--Sources/WireGuardApp/LocalizationHelper.swift12
-rw-r--r--Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_22x29.pngbin0 -> 953 bytes
-rw-r--r--Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_320x320.pngbin0 -> 19123 bytes
-rw-r--r--Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_44x58.pngbin0 -> 1359 bytes
-rw-r--r--Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_64x64.pngbin0 -> 3364 bytes
-rw-r--r--Sources/WireGuardApp/Tunnel/ActivateOnDemandOption.swift133
-rw-r--r--Sources/WireGuardApp/Tunnel/MockTunnels.swift49
-rw-r--r--Sources/WireGuardApp/Tunnel/TunnelConfiguration+UapiConfig.swift185
-rw-r--r--Sources/WireGuardApp/Tunnel/TunnelErrors.swift107
-rw-r--r--Sources/WireGuardApp/Tunnel/TunnelStatus.swift63
-rw-r--r--Sources/WireGuardApp/Tunnel/TunnelsManager.swift807
-rw-r--r--Sources/WireGuardApp/UI/ActivateOnDemandViewModel.swift198
-rw-r--r--Sources/WireGuardApp/UI/ErrorPresenterProtocol.swift25
-rw-r--r--Sources/WireGuardApp/UI/LogViewHelper.swift57
-rw-r--r--Sources/WireGuardApp/UI/PrivateDataConfirmation.swift37
-rw-r--r--Sources/WireGuardApp/UI/TunnelImporter.swift95
-rw-r--r--Sources/WireGuardApp/UI/TunnelViewModel.swift696
-rw-r--r--Sources/WireGuardApp/UI/iOS/AppDelegate.swift89
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json116
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo.pngbin0 -> 81773 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@1x.pngbin0 -> 865 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@2x-1.pngbin0 -> 1508 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@2x.pngbin0 -> 1508 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@3x.pngbin0 -> 3133 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@1x.pngbin0 -> 1166 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@2x-1.pngbin0 -> 3012 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@2x.pngbin0 -> 3012 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@3x.pngbin0 -> 4615 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@1x.pngbin0 -> 1508 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@2x-1.pngbin0 -> 4188 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@2x.pngbin0 -> 4188 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@3x.pngbin0 -> 6518 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_60pt@2x.pngbin0 -> 6518 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_60pt@3x.pngbin0 -> 10037 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_76pt@1x.pngbin0 -> 3953 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_76pt@2x.pngbin0 -> 8347 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_83.5pt@2x.pngbin0 -> 9329 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/Contents.json6
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/wireguard.imageset/Contents.json15
-rw-r--r--Sources/WireGuardApp/UI/iOS/Assets.xcassets/wireguard.imageset/wireguard.pdfbin0 -> 8895 bytes
-rw-r--r--Sources/WireGuardApp/UI/iOS/Base.lproj/LaunchScreen.storyboard82
-rw-r--r--Sources/WireGuardApp/UI/iOS/ConfirmationAlertPresenter.swift25
-rw-r--r--Sources/WireGuardApp/UI/iOS/ErrorPresenter.swift19
-rw-r--r--Sources/WireGuardApp/UI/iOS/Info.plist131
-rw-r--r--Sources/WireGuardApp/UI/iOS/QuickActionItem.swift25
-rw-r--r--Sources/WireGuardApp/UI/iOS/RecentTunnelsTracker.swift72
-rw-r--r--Sources/WireGuardApp/UI/iOS/UITableViewCell+Reuse.swift21
-rw-r--r--Sources/WireGuardApp/UI/iOS/View/BorderedTextButton.swift51
-rw-r--r--Sources/WireGuardApp/UI/iOS/View/ButtonCell.swift55
-rw-r--r--Sources/WireGuardApp/UI/iOS/View/CheckmarkCell.swift31
-rw-r--r--Sources/WireGuardApp/UI/iOS/View/ChevronCell.swift30
-rw-r--r--Sources/WireGuardApp/UI/iOS/View/EditableTextCell.swift64
-rw-r--r--Sources/WireGuardApp/UI/iOS/View/KeyValueCell.swift236
-rw-r--r--Sources/WireGuardApp/UI/iOS/View/SwitchCell.swift56
-rw-r--r--Sources/WireGuardApp/UI/iOS/View/TextCell.swift38
-rw-r--r--Sources/WireGuardApp/UI/iOS/View/TunnelEditKeyValueCell.swift52
-rw-r--r--Sources/WireGuardApp/UI/iOS/View/TunnelListCell.swift122
-rw-r--r--Sources/WireGuardApp/UI/iOS/ViewController/LogViewController.swift165
-rw-r--r--Sources/WireGuardApp/UI/iOS/ViewController/MainViewController.swift146
-rw-r--r--Sources/WireGuardApp/UI/iOS/ViewController/QRScanViewController.swift156
-rw-r--r--Sources/WireGuardApp/UI/iOS/ViewController/SSIDOptionDetailTableViewController.swift49
-rw-r--r--Sources/WireGuardApp/UI/iOS/ViewController/SSIDOptionEditTableViewController.swift303
-rw-r--r--Sources/WireGuardApp/UI/iOS/ViewController/SettingsTableViewController.swift185
-rw-r--r--Sources/WireGuardApp/UI/iOS/ViewController/TunnelDetailTableViewController.swift458
-rw-r--r--Sources/WireGuardApp/UI/iOS/ViewController/TunnelEditTableViewController.swift504
-rw-r--r--Sources/WireGuardApp/UI/iOS/ViewController/TunnelsListTableViewController.swift429
-rw-r--r--Sources/WireGuardApp/UI/iOS/WireGuard.entitlements16
-rw-r--r--Sources/WireGuardApp/UI/macOS/AppDelegate.swift252
-rw-r--r--Sources/WireGuardApp/UI/macOS/Application.swift19
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/Contents.json68
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon.pngbin0 -> 90032 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon128.pngbin0 -> 10281 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon16.pngbin0 -> 1161 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon256-1.pngbin0 -> 22477 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon256.pngbin0 -> 22477 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon32-1.pngbin0 -> 2385 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon32.pngbin0 -> 2385 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon512-1.pngbin0 -> 50192 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon512.pngbin0 -> 50192 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon64.pngbin0 -> 4991 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/Contents.json6
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/Contents.json26
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@1x.pngbin0 -> 978 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@2x.pngbin0 -> 1722 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@3x.pngbin0 -> 1975 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/Contents.json26
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@1x.pngbin0 -> 881 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@2x.pngbin0 -> 1390 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@3x.pngbin0 -> 1581 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/Contents.json26
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/StatusBarIconDot1@1x.pngbin0 -> 953 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/StatusBarIconDot1@2x.pngbin0 -> 1570 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/StatusBarIconDot1@3x.pngbin0 -> 1744 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/Contents.json26
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/StatusBarIconDot2@1x.pngbin0 -> 942 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/StatusBarIconDot2@2x.pngbin0 -> 1502 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/StatusBarIconDot2@3x.pngbin0 -> 1676 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/Contents.json26
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/StatusBarIconDot3@1x.pngbin0 -> 958 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/StatusBarIconDot3@2x.pngbin0 -> 1521 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/StatusBarIconDot3@3x.pngbin0 -> 1677 bytes
-rw-r--r--Sources/WireGuardApp/UI/macOS/ErrorPresenter.swift23
-rw-r--r--Sources/WireGuardApp/UI/macOS/ImportPanelPresenter.swift19
-rw-r--r--Sources/WireGuardApp/UI/macOS/Info.plist40
-rw-r--r--Sources/WireGuardApp/UI/macOS/LaunchedAtLoginDetector.swift28
-rw-r--r--Sources/WireGuardApp/UI/macOS/LoginItemHelper/Info.plist36
-rw-r--r--Sources/WireGuardApp/UI/macOS/LoginItemHelper/LoginItemHelper.entitlements8
-rw-r--r--Sources/WireGuardApp/UI/macOS/LoginItemHelper/main.m17
-rw-r--r--Sources/WireGuardApp/UI/macOS/MacAppStoreUpdateDetector.swift35
-rw-r--r--Sources/WireGuardApp/UI/macOS/MainMenu.swift130
-rw-r--r--Sources/WireGuardApp/UI/macOS/NSColor+Hex.swift25
-rw-r--r--Sources/WireGuardApp/UI/macOS/NSTableView+Reuse.swift17
-rw-r--r--Sources/WireGuardApp/UI/macOS/ParseError+WireGuardAppError.swift57
-rw-r--r--Sources/WireGuardApp/UI/macOS/StatusItemController.swift64
-rw-r--r--Sources/WireGuardApp/UI/macOS/StatusMenu.swift234
-rw-r--r--Sources/WireGuardApp/UI/macOS/TunnelsTracker.swift119
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/ButtonRow.swift67
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/ConfTextColorTheme.swift47
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/ConfTextStorage.swift180
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/ConfTextView.swift89
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/DeleteTunnelsConfirmationAlert.swift36
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/KeyValueRow.swift139
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/LogViewCell.swift66
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/OnDemandWiFiControls.swift171
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/TunnelListRow.swift75
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/highlighter.c641
-rw-r--r--Sources/WireGuardApp/UI/macOS/View/highlighter.h38
-rw-r--r--Sources/WireGuardApp/UI/macOS/ViewController/ButtonedDetailViewController.swift54
-rw-r--r--Sources/WireGuardApp/UI/macOS/ViewController/LogViewController.swift274
-rw-r--r--Sources/WireGuardApp/UI/macOS/ViewController/ManageTunnelsRootViewController.swift135
-rw-r--r--Sources/WireGuardApp/UI/macOS/ViewController/TunnelDetailTableViewController.swift516
-rw-r--r--Sources/WireGuardApp/UI/macOS/ViewController/TunnelEditViewController.swift297
-rw-r--r--Sources/WireGuardApp/UI/macOS/ViewController/TunnelsListTableViewController.swift354
-rw-r--r--Sources/WireGuardApp/UI/macOS/ViewController/UnusableTunnelDetailViewController.swift71
-rw-r--r--Sources/WireGuardApp/UI/macOS/WireGuard.entitlements18
-rw-r--r--Sources/WireGuardApp/WireGuard-Bridging-Header.h9
-rw-r--r--Sources/WireGuardApp/WireGuardAppError.swift8
-rw-r--r--Sources/WireGuardApp/WireGuardResult.swift28
-rw-r--r--Sources/WireGuardApp/ZipArchive/3rdparty/minizip/MiniZip64_info.txt74
-rw-r--r--Sources/WireGuardApp/ZipArchive/3rdparty/minizip/ioapi.c247
-rw-r--r--Sources/WireGuardApp/ZipArchive/3rdparty/minizip/ioapi.h208
-rw-r--r--Sources/WireGuardApp/ZipArchive/3rdparty/minizip/unzip.c2071
-rw-r--r--Sources/WireGuardApp/ZipArchive/3rdparty/minizip/unzip.h437
-rw-r--r--Sources/WireGuardApp/ZipArchive/3rdparty/minizip/zip.c1956
-rw-r--r--Sources/WireGuardApp/ZipArchive/3rdparty/minizip/zip.h362
-rw-r--r--Sources/WireGuardApp/ZipArchive/ZipArchive.swift105
-rw-r--r--Sources/WireGuardApp/ZipArchive/ZipExporter.swift44
-rw-r--r--Sources/WireGuardApp/ZipArchive/ZipImporter.swift46
-rw-r--r--Sources/WireGuardApp/de.lproj/Localizable.strings445
-rw-r--r--Sources/WireGuardApp/it.lproj/Localizable.strings445
-rw-r--r--Sources/WireGuardApp/ja.lproj/Localizable.strings445
156 files changed, 18202 insertions, 0 deletions
diff --git a/Sources/WireGuardApp/Base.lproj/InfoPlist.strings b/Sources/WireGuardApp/Base.lproj/InfoPlist.strings
new file mode 100644
index 0000000..b7ba394
--- /dev/null
+++ b/Sources/WireGuardApp/Base.lproj/InfoPlist.strings
@@ -0,0 +1,7 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+// iOS permission prompts
+
+NSCameraUsageDescription = "Camera is used for scanning QR codes for importing WireGuard configurations";
+NSFaceIDUsageDescription = "Face ID is used for authenticating viewing and exporting of private keys";
diff --git a/Sources/WireGuardApp/Base.lproj/Localizable.strings b/Sources/WireGuardApp/Base.lproj/Localizable.strings
new file mode 100644
index 0000000..1cc9693
--- /dev/null
+++ b/Sources/WireGuardApp/Base.lproj/Localizable.strings
@@ -0,0 +1,445 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+// Generic alert action names
+
+"actionOK" = "OK";
+"actionCancel" = "Cancel";
+"actionSave" = "Save";
+
+// Tunnels list UI
+
+"tunnelsListTitle" = "WireGuard";
+"tunnelsListSettingsButtonTitle" = "Settings";
+"tunnelsListCenteredAddTunnelButtonTitle" = "Add a tunnel";
+"tunnelsListSwipeDeleteButtonTitle" = "Delete";
+"tunnelsListSelectButtonTitle" = "Select";
+"tunnelsListSelectAllButtonTitle" = "Select All";
+"tunnelsListDeleteButtonTitle" = "Delete";
+"tunnelsListSelectedTitle (%d)" = "%d selected";
+
+// Tunnels list menu
+
+"addTunnelMenuHeader" = "Add a new WireGuard tunnel";
+"addTunnelMenuImportFile" = "Create from file or archive";
+"addTunnelMenuQRCode" = "Create from QR code";
+"addTunnelMenuFromScratch" = "Create from scratch";
+
+// Tunnels list alerts
+
+"alertImportedFromMultipleFilesTitle (%d)" = "Created %d tunnels";
+"alertImportedFromMultipleFilesMessage (%1$d of %2$d)" = "Created %1$d of %2$d tunnels from imported files";
+
+"alertImportedFromZipTitle (%d)" = "Created %d tunnels";
+"alertImportedFromZipMessage (%1$d of %2$d)" = "Created %1$d of %2$d tunnels from zip archive";
+
+"alertBadConfigImportTitle" = "Unable to import tunnel";
+"alertBadConfigImportMessage (%@)" = "The file ‘%@’ does not contain a valid WireGuard configuration";
+
+"deleteTunnelsConfirmationAlertButtonTitle" = "Delete";
+"deleteTunnelConfirmationAlertButtonMessage (%d)" = "Delete %d tunnel?";
+"deleteTunnelsConfirmationAlertButtonMessage (%d)" = "Delete %d tunnels?";
+
+// Tunnel detail and edit UI
+
+"newTunnelViewTitle" = "New configuration";
+"editTunnelViewTitle" = "Edit configuration";
+
+"tunnelSectionTitleStatus" = "Status";
+
+"tunnelStatusInactive" = "Inactive";
+"tunnelStatusActivating" = "Activating";
+"tunnelStatusActive" = "Active";
+"tunnelStatusDeactivating" = "Deactivating";
+"tunnelStatusReasserting" = "Reactivating";
+"tunnelStatusRestarting" = "Restarting";
+"tunnelStatusWaiting" = "Waiting";
+
+"macToggleStatusButtonActivate" = "Activate";
+"macToggleStatusButtonActivating" = "Activating…";
+"macToggleStatusButtonDeactivate" = "Deactivate";
+"macToggleStatusButtonDeactivating" = "Deactivating…";
+"macToggleStatusButtonReasserting" = "Reactivating…";
+"macToggleStatusButtonRestarting" = "Restarting…";
+"macToggleStatusButtonWaiting" = "Waiting…";
+
+"tunnelSectionTitleInterface" = "Interface";
+
+"tunnelInterfaceName" = "Name";
+"tunnelInterfacePrivateKey" = "Private key";
+"tunnelInterfacePublicKey" = "Public key";
+"tunnelInterfaceGenerateKeypair" = "Generate keypair";
+"tunnelInterfaceAddresses" = "Addresses";
+"tunnelInterfaceListenPort" = "Listen port";
+"tunnelInterfaceMTU" = "MTU";
+"tunnelInterfaceDNS" = "DNS servers";
+"tunnelInterfaceStatus" = "Status";
+
+"tunnelSectionTitlePeer" = "Peer";
+
+"tunnelPeerPublicKey" = "Public key";
+"tunnelPeerPreSharedKey" = "Preshared key";
+"tunnelPeerEndpoint" = "Endpoint";
+"tunnelPeerPersistentKeepalive" = "Persistent keepalive";
+"tunnelPeerAllowedIPs" = "Allowed IPs";
+"tunnelPeerRxBytes" = "Data received";
+"tunnelPeerTxBytes" = "Data sent";
+"tunnelPeerLastHandshakeTime" = "Latest handshake";
+"tunnelPeerExcludePrivateIPs" = "Exclude private IPs";
+
+"tunnelSectionTitleOnDemand" = "On-Demand Activation";
+
+"tunnelOnDemandCellular" = "Cellular";
+"tunnelOnDemandEthernet" = "Ethernet";
+"tunnelOnDemandWiFi" = "Wi-Fi";
+"tunnelOnDemandSSIDsKey" = "SSIDs";
+
+"tunnelOnDemandAnySSID" = "Any SSID";
+"tunnelOnDemandOnlyTheseSSIDs" = "Only these SSIDs";
+"tunnelOnDemandExceptTheseSSIDs" = "Except these SSIDs";
+"tunnelOnDemandOnlySSID (%d)" = "Only %d SSID";
+"tunnelOnDemandOnlySSIDs (%d)" = "Only %d SSIDs";
+"tunnelOnDemandExceptSSID (%d)" = "Except %d SSID";
+"tunnelOnDemandExceptSSIDs (%d)" = "Except %d SSIDs";
+"tunnelOnDemandSSIDOptionDescriptionMac (%1$@: %2$@)" = "%1$@: %2$@";
+
+"tunnelOnDemandSSIDViewTitle" = "SSIDs";
+"tunnelOnDemandSectionTitleSelectedSSIDs" = "SSIDs";
+"tunnelOnDemandNoSSIDs" = "No SSIDs";
+"tunnelOnDemandSectionTitleAddSSIDs" = "Add SSIDs";
+"tunnelOnDemandAddMessageAddConnectedSSID (%@)" = "Add connected: %@";
+"tunnelOnDemandAddMessageAddNewSSID" = "Add new";
+
+"tunnelOnDemandKey" = "On demand";
+"tunnelOnDemandOptionOff" = "Off";
+"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi only";
+"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi or cellular";
+"tunnelOnDemandOptionCellularOnly" = "Cellular only";
+"tunnelOnDemandOptionWiFiOrEthernet" = "Wi-Fi or ethernet";
+"tunnelOnDemandOptionEthernetOnly" = "Ethernet only";
+
+"addPeerButtonTitle" = "Add peer";
+
+"deletePeerButtonTitle" = "Delete peer";
+"deletePeerConfirmationAlertButtonTitle" = "Delete";
+"deletePeerConfirmationAlertMessage" = "Delete this peer?";
+
+"deleteTunnelButtonTitle" = "Delete tunnel";
+"deleteTunnelConfirmationAlertButtonTitle" = "Delete";
+"deleteTunnelConfirmationAlertMessage" = "Delete this tunnel?";
+
+"tunnelEditPlaceholderTextRequired" = "Required";
+"tunnelEditPlaceholderTextOptional" = "Optional";
+"tunnelEditPlaceholderTextAutomatic" = "Automatic";
+"tunnelEditPlaceholderTextStronglyRecommended" = "Strongly recommended";
+"tunnelEditPlaceholderTextOff" = "Off";
+
+"tunnelPeerPersistentKeepaliveValue (%@)" = "every %@ seconds";
+"tunnelHandshakeTimestampNow" = "Now";
+"tunnelHandshakeTimestampSystemClockBackward" = "(System clock wound backwards)";
+"tunnelHandshakeTimestampAgo (%@)" = "%@ ago";
+"tunnelHandshakeTimestampYear (%d)" = "%d year";
+"tunnelHandshakeTimestampYears (%d)" = "%d years";
+"tunnelHandshakeTimestampDay (%d)" = "%d day";
+"tunnelHandshakeTimestampDays (%d)" = "%d days";
+"tunnelHandshakeTimestampHour (%d)" = "%d hour";
+"tunnelHandshakeTimestampHours (%d)" = "%d hours";
+"tunnelHandshakeTimestampMinute (%d)" = "%d minute";
+"tunnelHandshakeTimestampMinutes (%d)" = "%d minutes";
+"tunnelHandshakeTimestampSecond (%d)" = "%d second";
+"tunnelHandshakeTimestampSeconds (%d)" = "%d seconds";
+
+"tunnelHandshakeTimestampHours hh:mm:ss (%@)" = "%@ hours";
+"tunnelHandshakeTimestampMinutes mm:ss (%@)" = "%@ minutes";
+
+"tunnelPeerPresharedKeyEnabled" = "enabled";
+
+// Error alerts while creating / editing a tunnel configuration
+
+/* Alert title for error in the interface data */
+"alertInvalidInterfaceTitle" = "Invalid interface";
+
+/* Any one of the following alert messages can go with the above title */
+"alertInvalidInterfaceMessageNameRequired" = "Interface name is required";
+"alertInvalidInterfaceMessagePrivateKeyRequired" = "Interface’s private key is required";
+"alertInvalidInterfaceMessagePrivateKeyInvalid" = "Interface’s private key must be a 32-byte key in base64 encoding";
+"alertInvalidInterfaceMessageAddressInvalid" = "Interface addresses must be a list of comma-separated IP addresses, optionally in CIDR notation";
+"alertInvalidInterfaceMessageListenPortInvalid" = "Interface’s listen port must be between 0 and 65535, or unspecified";
+"alertInvalidInterfaceMessageMTUInvalid" = "Interface’s MTU must be between 576 and 65535, or unspecified";
+"alertInvalidInterfaceMessageDNSInvalid" = "Interface’s DNS servers must be a list of comma-separated IP addresses";
+
+/* Alert title for error in the peer data */
+"alertInvalidPeerTitle" = "Invalid peer";
+
+/* Any one of the following alert messages can go with the above title */
+"alertInvalidPeerMessagePublicKeyRequired" = "Peer’s public key is required";
+"alertInvalidPeerMessagePublicKeyInvalid" = "Peer’s public key must be a 32-byte key in base64 encoding";
+"alertInvalidPeerMessagePreSharedKeyInvalid" = "Peer’s preshared key must be a 32-byte key in base64 encoding";
+"alertInvalidPeerMessageAllowedIPsInvalid" = "Peer’s allowed IPs must be a list of comma-separated IP addresses, optionally in CIDR notation";
+"alertInvalidPeerMessageEndpointInvalid" = "Peer’s endpoint must be of the form ‘host:port’ or ‘[host]:port’";
+"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "Peer’s persistent keepalive must be between 0 to 65535, or unspecified";
+"alertInvalidPeerMessagePublicKeyDuplicated" = "Two or more peers cannot have the same public key";
+
+// Scanning QR code UI
+
+"scanQRCodeViewTitle" = "Scan QR code";
+"scanQRCodeTipText" = "Tip: Generate with `qrencode -t ansiutf8 < tunnel.conf`";
+
+// Scanning QR code alerts
+
+"alertScanQRCodeCameraUnsupportedTitle" = "Camera Unsupported";
+"alertScanQRCodeCameraUnsupportedMessage" = "This device is not able to scan QR codes";
+
+"alertScanQRCodeInvalidQRCodeTitle" = "Invalid QR Code";
+"alertScanQRCodeInvalidQRCodeMessage" = "The scanned QR code is not a valid WireGuard configuration";
+
+"alertScanQRCodeUnreadableQRCodeTitle" = "Invalid Code";
+"alertScanQRCodeUnreadableQRCodeMessage" = "The scanned code could not be read";
+
+"alertScanQRCodeNamePromptTitle" = "Please name the scanned tunnel";
+
+// Settings UI
+
+"settingsViewTitle" = "Settings";
+
+"settingsSectionTitleAbout" = "About";
+"settingsVersionKeyWireGuardForIOS" = "WireGuard for iOS";
+"settingsVersionKeyWireGuardGoBackend" = "WireGuard Go Backend";
+
+"settingsSectionTitleExportConfigurations" = "Export configurations";
+"settingsExportZipButtonTitle" = "Export zip archive";
+
+"settingsSectionTitleTunnelLog" = "Log";
+"settingsViewLogButtonTitle" = "View log";
+
+// Log view
+
+"logViewTitle" = "Log";
+
+// Log alerts
+
+"alertUnableToRemovePreviousLogTitle" = "Log export failed";
+"alertUnableToRemovePreviousLogMessage" = "The pre-existing log could not be cleared";
+
+"alertUnableToWriteLogTitle" = "Log export failed";
+"alertUnableToWriteLogMessage" = "Unable to write logs to file";
+
+// Zip import / export error alerts
+
+"alertCantOpenInputZipFileTitle" = "Unable to read zip archive";
+"alertCantOpenInputZipFileMessage" = "The zip archive could not be read.";
+
+"alertCantOpenOutputZipFileForWritingTitle" = "Unable to create zip archive";
+"alertCantOpenOutputZipFileForWritingMessage" = "Could not open zip file for writing.";
+
+"alertBadArchiveTitle" = "Unable to read zip archive";
+"alertBadArchiveMessage" = "Bad or corrupt zip archive.";
+
+"alertNoTunnelsToExportTitle" = "Nothing to export";
+"alertNoTunnelsToExportMessage" = "There are no tunnels to export";
+
+"alertNoTunnelsInImportedZipArchiveTitle" = "No tunnels in zip archive";
+"alertNoTunnelsInImportedZipArchiveMessage" = "No .conf tunnel files were found inside the zip archive.";
+
+// Conf import error alerts
+
+"alertCantOpenInputConfFileTitle" = "Unable to import from file";
+"alertCantOpenInputConfFileMessage (%@)" = "The file ‘%@’ could not be read.";
+
+// Tunnel management error alerts
+
+"alertTunnelActivationFailureTitle" = "Activation failure";
+"alertTunnelActivationFailureMessage" = "The tunnel could not be activated. Please ensure that you are connected to the Internet.";
+"alertTunnelActivationSavedConfigFailureMessage" = "Unable to retrieve tunnel information from the saved configuration.";
+"alertTunnelActivationBackendFailureMessage" = "Unable to turn on Go backend library.";
+"alertTunnelActivationFileDescriptorFailureMessage" = "Unable to determine TUN device file descriptor.";
+"alertTunnelActivationSetNetworkSettingsMessage" = "Unable to apply network settings to tunnel object.";
+
+"alertTunnelActivationFailureOnDemandAddendum" = " This tunnel has Activate On Demand enabled, so this tunnel might be re-activated automatically by the OS. You may turn off Activate On Demand in this app by editing the tunnel configuration.";
+
+"alertTunnelDNSFailureTitle" = "DNS resolution failure";
+"alertTunnelDNSFailureMessage" = "One or more endpoint domains could not be resolved.";
+
+"alertTunnelNameEmptyTitle" = "No name provided";
+"alertTunnelNameEmptyMessage" = "Cannot create tunnel with an empty name";
+
+"alertTunnelAlreadyExistsWithThatNameTitle" = "Name already exists";
+"alertTunnelAlreadyExistsWithThatNameMessage" = "A tunnel with that name already exists";
+
+"alertTunnelActivationErrorTunnelIsNotInactiveTitle" = "Activation in progress";
+"alertTunnelActivationErrorTunnelIsNotInactiveMessage" = "The tunnel is already active or in the process of being activated";
+
+// Tunnel management error alerts on system error
+
+/* The alert message that goes with the following titles would be
+ one of the alertSystemErrorMessage* listed further down */
+"alertSystemErrorOnListingTunnelsTitle" = "Unable to list tunnels";
+"alertSystemErrorOnAddTunnelTitle" = "Unable to create tunnel";
+"alertSystemErrorOnModifyTunnelTitle" = "Unable to modify tunnel";
+"alertSystemErrorOnRemoveTunnelTitle" = "Unable to remove tunnel";
+
+/* The alert message for this alert shall include
+ one of the alertSystemErrorMessage* listed further down */
+"alertTunnelActivationSystemErrorTitle" = "Activation failure";
+"alertTunnelActivationSystemErrorMessage (%@)" = "The tunnel could not be activated. %@";
+
+/* alertSystemErrorMessage* messages */
+"alertSystemErrorMessageTunnelConfigurationInvalid" = "The configuration is invalid.";
+"alertSystemErrorMessageTunnelConfigurationDisabled" = "The configuration is disabled.";
+"alertSystemErrorMessageTunnelConnectionFailed" = "The connection failed.";
+"alertSystemErrorMessageTunnelConfigurationStale" = "The configuration is stale.";
+"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Reading or writing the configuration failed.";
+"alertSystemErrorMessageTunnelConfigurationUnknown" = "Unknown system error.";
+
+// Mac status bar menu / pulldown menu / main menu
+
+"macMenuNetworks (%@)" = "Networks: %@";
+"macMenuNetworksNone" = "Networks: None";
+
+"macMenuTitle" = "WireGuard";
+"macMenuManageTunnels" = "Manage Tunnels";
+"macMenuImportTunnels" = "Import Tunnel(s) from File…";
+"macMenuAddEmptyTunnel" = "Add Empty Tunnel…";
+"macMenuViewLog" = "View Log";
+"macMenuExportTunnels" = "Export Tunnels to Zip…";
+"macMenuAbout" = "About WireGuard";
+"macMenuQuit" = "Quit WireGuard";
+
+"macMenuHideApp" = "Hide WireGuard";
+"macMenuHideOtherApps" = "Hide Others";
+"macMenuShowAllApps" = "Show All";
+
+"macMenuFile" = "File";
+"macMenuCloseWindow" = "Close Window";
+
+"macMenuEdit" = "Edit";
+"macMenuCut" = "Cut";
+"macMenuCopy" = "Copy";
+"macMenuPaste" = "Paste";
+"macMenuSelectAll" = "Select All";
+
+"macMenuTunnel" = "Tunnel";
+"macMenuToggleStatus" = "Toggle Status";
+"macMenuEditTunnel" = "Edit…";
+"macMenuDeleteSelected" = "Delete Selected";
+
+"macMenuWindow" = "Window";
+"macMenuMinimize" = "Minimize";
+"macMenuZoom" = "Zoom";
+
+// Mac manage tunnels window
+
+"macWindowTitleManageTunnels" = "Manage WireGuard Tunnels";
+
+"macDeleteTunnelConfirmationAlertMessage (%@)" = "Are you sure you want to delete ‘%@’?";
+"macDeleteMultipleTunnelsConfirmationAlertMessage (%d)" = "Are you sure you want to delete %d tunnels?";
+"macDeleteTunnelConfirmationAlertInfo" = "You cannot undo this action.";
+"macDeleteTunnelConfirmationAlertButtonTitleDelete" = "Delete";
+"macDeleteTunnelConfirmationAlertButtonTitleCancel" = "Cancel";
+"macDeleteTunnelConfirmationAlertButtonTitleDeleting" = "Deleting…";
+
+"macButtonImportTunnels" = "Import tunnel(s) from file";
+"macSheetButtonImport" = "Import";
+
+"macNameFieldExportLog" = "Save log to:";
+"macSheetButtonExportLog" = "Save";
+
+"macNameFieldExportZip" = "Export tunnels to:";
+"macSheetButtonExportZip" = "Save";
+
+"macButtonDeleteTunnels (%d)" = "Delete %d tunnels";
+
+"macButtonEdit" = "Edit";
+
+// Mac detail/edit view fields
+
+"macFieldKey (%@)" = "%@:";
+"macFieldOnDemand" = "On-Demand:";
+"macFieldOnDemandSSIDs" = "SSIDs:";
+
+// Mac status display
+
+"macStatus (%@)" = "Status: %@";
+
+// Mac editing config
+
+"macEditDiscard" = "Discard";
+"macEditSave" = "Save";
+
+"macAlertNameIsEmpty" = "Name is required";
+"macAlertDuplicateName (%@)" = "Another tunnel already exists with the name ‘%@’.";
+
+"macAlertInvalidLine (%@)" = "Invalid line: ‘%@’.";
+
+"macAlertNoInterface" = "Configuration must have an ‘Interface’ section.";
+"macAlertMultipleInterfaces" = "Configuration must have only one ‘Interface’ section.";
+"macAlertPrivateKeyInvalid" = "Private key is invalid.";
+"macAlertListenPortInvalid (%@)" = "Listen port ‘%@’ is invalid.";
+"macAlertAddressInvalid (%@)" = "Address ‘%@’ is invalid.";
+"macAlertDNSInvalid (%@)" = "DNS ‘%@’ is invalid.";
+"macAlertMTUInvalid (%@)" = "MTU ‘%@’ is invalid.";
+
+"macAlertUnrecognizedInterfaceKey (%@)" = "Interface contains unrecognized key ‘%@’";
+"macAlertInfoUnrecognizedInterfaceKey" = "Valid keys are: ‘PrivateKey’, ‘ListenPort’, ‘Address’, ‘DNS’ and ‘MTU’.";
+
+"macAlertPublicKeyInvalid" = "Public key is invalid";
+"macAlertPreSharedKeyInvalid" = "Preshared key is invalid";
+"macAlertAllowedIPInvalid (%@)" = "Allowed IP ‘%@’ is invalid";
+"macAlertEndpointInvalid (%@)" = "Endpoint ‘%@’ is invalid";
+"macAlertPersistentKeepliveInvalid (%@)" = "Persistent keepalive value ‘%@’ is invalid";
+
+"macAlertUnrecognizedPeerKey (%@)" = "Peer contains unrecognized key ‘%@’";
+"macAlertInfoUnrecognizedPeerKey" = "Valid keys are: ‘PublicKey’, ‘PresharedKey’, ‘AllowedIPs’, ‘Endpoint’ and ‘PersistentKeepalive’";
+
+"macAlertMultipleEntriesForKey (%@)" = "There should be only one entry per section for key ‘%@’";
+
+// Mac about dialog
+
+"macAppVersion (%@)" = "App version: %@";
+"macGoBackendVersion (%@)" = "Go backend version: %@";
+
+// Privacy
+
+"macExportPrivateData" = "export tunnel private keys";
+"macViewPrivateData" = "view tunnel private keys";
+"iosExportPrivateData" = "Authenticate to export tunnel private keys.";
+"iosViewPrivateData" = "Authenticate to view tunnel private keys.";
+
+// Mac alert
+
+"macConfirmAndQuitAlertMessage" = "Do you want to close the tunnels manager or quit WireGuard entirely?";
+"macConfirmAndQuitAlertInfo" = "If you close the tunnels manager, WireGuard will continue to be available from the menu bar icon.";
+"macConfirmAndQuitInfoWithActiveTunnel (%@)" = "If you close the tunnels manager, WireGuard will continue to be available from the menu bar icon.\n\nNote that if you quit WireGuard entirely the currently active tunnel ('%@') will still remain active until you deactivate it from this application or through the Network panel in System Preferences.";
+"macConfirmAndQuitAlertQuitWireGuard" = "Quit WireGuard";
+"macConfirmAndQuitAlertCloseWindow" = "Close Tunnels Manager";
+
+"macAppExitingWithActiveTunnelMessage" = "WireGuard is exiting with an active tunnel";
+"macAppExitingWithActiveTunnelInfo" = "The tunnel will remain active after exiting. You may disable it by reopening this application or through the Network panel in System Preferences.";
+
+// Mac tooltip
+
+"macToolTipEditTunnel" = "Edit tunnel (⌘E)";
+"macToolTipToggleStatus" = "Toggle status (⌘T)";
+
+// Mac log view
+
+"macLogColumnTitleTime" = "Time";
+"macLogColumnTitleLogMessage" = "Log message";
+"macLogButtonTitleClose" = "Close";
+"macLogButtonTitleSave" = "Save…";
+
+// Mac unusable tunnel view
+
+"macUnusableTunnelMessage" = "The configuration for this tunnel cannot be found in the keychain.";
+"macUnusableTunnelInfo" = "In case this tunnel was created by another user, only that user can view, edit, or activate this tunnel.";
+"macUnusableTunnelButtonTitleDeleteTunnel" = "Delete tunnel";
+
+// Mac App Store updating alert
+
+"macAppStoreUpdatingAlertMessage" = "App Store would like to update WireGuard";
+"macAppStoreUpdatingAlertInfoWithOnDemand (%@)" = "Please disable on-demand for tunnel ‘%@’, deactivate it, and then continue updating in App Store.";
+"macAppStoreUpdatingAlertInfoWithoutOnDemand (%@)" = "Please deactivate tunnel ‘%@’ and then continue updating in App Store.";
+
+// Donation
+
+"donateLink" = "♥ Donate to the WireGuard Project";
diff --git a/Sources/WireGuardApp/Config/Config.xcconfig b/Sources/WireGuardApp/Config/Config.xcconfig
new file mode 100644
index 0000000..002b7ad
--- /dev/null
+++ b/Sources/WireGuardApp/Config/Config.xcconfig
@@ -0,0 +1,2 @@
+#include "Version.xcconfig"
+#include "Developer.xcconfig"
diff --git a/Sources/WireGuardApp/Config/Developer.xcconfig.template b/Sources/WireGuardApp/Config/Developer.xcconfig.template
new file mode 100644
index 0000000..f34b145
--- /dev/null
+++ b/Sources/WireGuardApp/Config/Developer.xcconfig.template
@@ -0,0 +1,10 @@
+// Developer.xcconfig
+
+// You Apple developer account's Team ID
+DEVELOPMENT_TEAM = <team_id>
+
+// The bundle identifier of the apps.
+// Should be an app id created at developer.apple.com
+// with Network Extensions capabilty.
+APP_ID_IOS = <app_id>
+APP_ID_MACOS = <app_id>
diff --git a/Sources/WireGuardApp/Config/Version.xcconfig b/Sources/WireGuardApp/Config/Version.xcconfig
new file mode 100644
index 0000000..59864d0
--- /dev/null
+++ b/Sources/WireGuardApp/Config/Version.xcconfig
@@ -0,0 +1,2 @@
+VERSION_NAME = 0.0.20200127
+VERSION_ID = 17
diff --git a/Sources/WireGuardApp/LocalizationHelper.swift b/Sources/WireGuardApp/LocalizationHelper.swift
new file mode 100644
index 0000000..ed21090
--- /dev/null
+++ b/Sources/WireGuardApp/LocalizationHelper.swift
@@ -0,0 +1,12 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+
+func tr(_ key: String) -> String {
+ return NSLocalizedString(key, comment: "")
+}
+
+func tr(format: String, _ arguments: CVarArg...) -> String {
+ return String(format: NSLocalizedString(format, comment: ""), arguments: arguments)
+}
diff --git a/Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_22x29.png b/Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_22x29.png
new file mode 100644
index 0000000..41644c7
--- /dev/null
+++ b/Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_22x29.png
Binary files differ
diff --git a/Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_320x320.png b/Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_320x320.png
new file mode 100644
index 0000000..8d5d95f
--- /dev/null
+++ b/Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_320x320.png
Binary files differ
diff --git a/Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_44x58.png b/Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_44x58.png
new file mode 100644
index 0000000..7ace770
--- /dev/null
+++ b/Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_44x58.png
Binary files differ
diff --git a/Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_64x64.png b/Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_64x64.png
new file mode 100644
index 0000000..7adfa43
--- /dev/null
+++ b/Sources/WireGuardApp/Resources/DocumentIcons/wireguard_doc_logo_64x64.png
Binary files differ
diff --git a/Sources/WireGuardApp/Tunnel/ActivateOnDemandOption.swift b/Sources/WireGuardApp/Tunnel/ActivateOnDemandOption.swift
new file mode 100644
index 0000000..d44e1d6
--- /dev/null
+++ b/Sources/WireGuardApp/Tunnel/ActivateOnDemandOption.swift
@@ -0,0 +1,133 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import NetworkExtension
+
+enum ActivateOnDemandOption: Equatable {
+ case off
+ case wiFiInterfaceOnly(ActivateOnDemandSSIDOption)
+ case nonWiFiInterfaceOnly
+ case anyInterface(ActivateOnDemandSSIDOption)
+}
+
+#if os(iOS)
+private let nonWiFiInterfaceType: NEOnDemandRuleInterfaceType = .cellular
+#elseif os(macOS)
+private let nonWiFiInterfaceType: NEOnDemandRuleInterfaceType = .ethernet
+#else
+#error("Unimplemented")
+#endif
+
+enum ActivateOnDemandSSIDOption: Equatable {
+ case anySSID
+ case onlySpecificSSIDs([String])
+ case exceptSpecificSSIDs([String])
+}
+
+extension ActivateOnDemandOption {
+ func apply(on tunnelProviderManager: NETunnelProviderManager) {
+ let rules: [NEOnDemandRule]?
+ switch self {
+ case .off:
+ rules = nil
+ case .wiFiInterfaceOnly(let ssidOption):
+ rules = ssidOnDemandRules(option: ssidOption) + [NEOnDemandRuleDisconnect(interfaceType: nonWiFiInterfaceType)]
+ case .nonWiFiInterfaceOnly:
+ rules = [NEOnDemandRuleConnect(interfaceType: nonWiFiInterfaceType), NEOnDemandRuleDisconnect(interfaceType: .wiFi)]
+ case .anyInterface(let ssidOption):
+ if case .anySSID = ssidOption {
+ rules = [NEOnDemandRuleConnect(interfaceType: .any)]
+ } else {
+ rules = ssidOnDemandRules(option: ssidOption) + [NEOnDemandRuleConnect(interfaceType: nonWiFiInterfaceType)]
+ }
+ }
+ tunnelProviderManager.onDemandRules = rules
+ tunnelProviderManager.isOnDemandEnabled = self != .off
+ }
+
+ init(from tunnelProviderManager: NETunnelProviderManager) {
+ if tunnelProviderManager.isOnDemandEnabled, let onDemandRules = tunnelProviderManager.onDemandRules {
+ self = ActivateOnDemandOption.create(from: onDemandRules)
+ } else {
+ self = .off
+ }
+ }
+
+ private static func create(from rules: [NEOnDemandRule]) -> ActivateOnDemandOption {
+ switch rules.count {
+ case 0:
+ return .off
+ case 1:
+ let rule = rules[0]
+ guard rule.action == .connect else { return .off }
+ return .anyInterface(.anySSID)
+ case 2:
+ guard let connectRule = rules.first(where: { $0.action == .connect }) else {
+ wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but no connect rule.")
+ return .off
+ }
+ guard let disconnectRule = rules.first(where: { $0.action == .disconnect }) else {
+ wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but no disconnect rule.")
+ return .off
+ }
+ if connectRule.interfaceTypeMatch == .wiFi && disconnectRule.interfaceTypeMatch == nonWiFiInterfaceType {
+ return .wiFiInterfaceOnly(.anySSID)
+ } else if connectRule.interfaceTypeMatch == nonWiFiInterfaceType && disconnectRule.interfaceTypeMatch == .wiFi {
+ return .nonWiFiInterfaceOnly
+ } else {
+ wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found but interface types are inconsistent.")
+ return .off
+ }
+ case 3:
+ guard let ssidRule = rules.first(where: { $0.interfaceTypeMatch == .wiFi && $0.ssidMatch != nil }) else { return .off }
+ guard let nonWiFiRule = rules.first(where: { $0.interfaceTypeMatch == nonWiFiInterfaceType }) else { return .off }
+ let ssids = ssidRule.ssidMatch!
+ switch (ssidRule.action, nonWiFiRule.action) {
+ case (.connect, .connect):
+ return .anyInterface(.onlySpecificSSIDs(ssids))
+ case (.connect, .disconnect):
+ return .wiFiInterfaceOnly(.onlySpecificSSIDs(ssids))
+ case (.disconnect, .connect):
+ return .anyInterface(.exceptSpecificSSIDs(ssids))
+ case (.disconnect, .disconnect):
+ return .wiFiInterfaceOnly(.exceptSpecificSSIDs(ssids))
+ default:
+ wg_log(.error, message: "Unexpected onDemandRules set on tunnel provider manager: \(rules.count) rules found")
+ return .off
+ }
+ default:
+ wg_log(.error, message: "Unexpected number of onDemandRules set on tunnel provider manager: \(rules.count) rules found")
+ return .off
+ }
+ }
+}
+
+private extension NEOnDemandRuleConnect {
+ convenience init(interfaceType: NEOnDemandRuleInterfaceType, ssids: [String]? = nil) {
+ self.init()
+ interfaceTypeMatch = interfaceType
+ ssidMatch = ssids
+ }
+}
+
+private extension NEOnDemandRuleDisconnect {
+ convenience init(interfaceType: NEOnDemandRuleInterfaceType, ssids: [String]? = nil) {
+ self.init()
+ interfaceTypeMatch = interfaceType
+ ssidMatch = ssids
+ }
+}
+
+private func ssidOnDemandRules(option: ActivateOnDemandSSIDOption) -> [NEOnDemandRule] {
+ switch option {
+ case .anySSID:
+ return [NEOnDemandRuleConnect(interfaceType: .wiFi)]
+ case .onlySpecificSSIDs(let ssids):
+ assert(!ssids.isEmpty)
+ return [NEOnDemandRuleConnect(interfaceType: .wiFi, ssids: ssids),
+ NEOnDemandRuleDisconnect(interfaceType: .wiFi)]
+ case .exceptSpecificSSIDs(let ssids):
+ return [NEOnDemandRuleDisconnect(interfaceType: .wiFi, ssids: ssids),
+ NEOnDemandRuleConnect(interfaceType: .wiFi)]
+ }
+}
diff --git a/Sources/WireGuardApp/Tunnel/MockTunnels.swift b/Sources/WireGuardApp/Tunnel/MockTunnels.swift
new file mode 100644
index 0000000..1ffa99c
--- /dev/null
+++ b/Sources/WireGuardApp/Tunnel/MockTunnels.swift
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import NetworkExtension
+import WireGuardKit
+
+// Creates mock tunnels for the iOS Simulator.
+
+#if targetEnvironment(simulator)
+class MockTunnels {
+ static let tunnelNames = [
+ "demo",
+ "edgesecurity",
+ "home",
+ "office",
+ "infra-fr",
+ "infra-us",
+ "krantz",
+ "metheny",
+ "frisell"
+ ]
+ static let address = "192.168.%d.%d/32"
+ static let dnsServers = ["8.8.8.8", "8.8.4.4"]
+ static let endpoint = "demo.wireguard.com:51820"
+ static let allowedIPs = "0.0.0.0/0"
+
+ static func createMockTunnels() -> [NETunnelProviderManager] {
+ return tunnelNames.map { tunnelName -> NETunnelProviderManager in
+
+ var interface = InterfaceConfiguration(privateKey: PrivateKey())
+ interface.addresses = [IPAddressRange(from: String(format: address, Int.random(in: 1 ... 10), Int.random(in: 1 ... 254)))!]
+ interface.dns = dnsServers.map { DNSServer(from: $0)! }
+
+ var peer = PeerConfiguration(publicKey: PrivateKey().publicKey)
+ peer.endpoint = Endpoint(from: endpoint)
+ peer.allowedIPs = [IPAddressRange(from: allowedIPs)!]
+
+ let tunnelConfiguration = TunnelConfiguration(name: tunnelName, interface: interface, peers: [peer])
+
+ let tunnelProviderManager = NETunnelProviderManager()
+ tunnelProviderManager.protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration)
+ tunnelProviderManager.localizedDescription = tunnelConfiguration.name
+ tunnelProviderManager.isEnabled = true
+
+ return tunnelProviderManager
+ }
+ }
+}
+#endif
diff --git a/Sources/WireGuardApp/Tunnel/TunnelConfiguration+UapiConfig.swift b/Sources/WireGuardApp/Tunnel/TunnelConfiguration+UapiConfig.swift
new file mode 100644
index 0000000..fcd6a31
--- /dev/null
+++ b/Sources/WireGuardApp/Tunnel/TunnelConfiguration+UapiConfig.swift
@@ -0,0 +1,185 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+import WireGuardKit
+
+extension TunnelConfiguration {
+ convenience init(fromUapiConfig uapiConfig: String, basedOn base: TunnelConfiguration? = nil) throws {
+ var interfaceConfiguration: InterfaceConfiguration?
+ var peerConfigurations = [PeerConfiguration]()
+
+ var lines = uapiConfig.split(separator: "\n")
+ lines.append("")
+
+ var parserState = ParserState.inInterfaceSection
+ var attributes = [String: String]()
+
+ for line in lines {
+ var key = ""
+ var value = ""
+
+ if !line.isEmpty {
+ guard let equalsIndex = line.firstIndex(of: "=") else { throw ParseError.invalidLine(line) }
+ key = String(line[..<equalsIndex])
+ value = String(line[line.index(equalsIndex, offsetBy: 1)...])
+ }
+
+ if line.isEmpty || key == "public_key" {
+ // Previous section has ended; process the attributes collected so far
+ if parserState == .inInterfaceSection {
+ let interface = try TunnelConfiguration.collate(interfaceAttributes: attributes)
+ guard interfaceConfiguration == nil else { throw ParseError.multipleInterfaces }
+ interfaceConfiguration = interface
+ parserState = .inPeerSection
+ } else if parserState == .inPeerSection {
+ let peer = try TunnelConfiguration.collate(peerAttributes: attributes)
+ peerConfigurations.append(peer)
+ }
+ attributes.removeAll()
+ if line.isEmpty {
+ break
+ }
+ }
+
+ if let presentValue = attributes[key] {
+ if key == "allowed_ip" {
+ attributes[key] = presentValue + "," + value
+ } else {
+ throw ParseError.multipleEntriesForKey(key)
+ }
+ } else {
+ attributes[key] = value
+ }
+
+ let interfaceSectionKeys: Set<String> = ["private_key", "listen_port", "fwmark"]
+ let peerSectionKeys: Set<String> = ["public_key", "preshared_key", "allowed_ip", "endpoint", "persistent_keepalive_interval", "last_handshake_time_sec", "last_handshake_time_nsec", "rx_bytes", "tx_bytes", "protocol_version"]
+
+ if parserState == .inInterfaceSection {
+ guard interfaceSectionKeys.contains(key) else {
+ throw ParseError.interfaceHasUnrecognizedKey(key)
+ }
+ }
+ if parserState == .inPeerSection {
+ guard peerSectionKeys.contains(key) else {
+ throw ParseError.peerHasUnrecognizedKey(key)
+ }
+ }
+ }
+
+ let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
+ let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
+ if peerPublicKeysArray.count != peerPublicKeysSet.count {
+ throw ParseError.multiplePeersWithSamePublicKey
+ }
+
+ interfaceConfiguration?.addresses = base?.interface.addresses ?? []
+ interfaceConfiguration?.dns = base?.interface.dns ?? []
+ interfaceConfiguration?.mtu = base?.interface.mtu
+
+ if let interfaceConfiguration = interfaceConfiguration {
+ self.init(name: base?.name, interface: interfaceConfiguration, peers: peerConfigurations)
+ } else {
+ throw ParseError.noInterface
+ }
+ }
+
+ private static func collate(interfaceAttributes attributes: [String: String]) throws -> InterfaceConfiguration {
+ guard let privateKeyString = attributes["private_key"] else {
+ throw ParseError.interfaceHasNoPrivateKey
+ }
+ guard let privateKey = PrivateKey(hexKey: privateKeyString) else {
+ throw ParseError.interfaceHasInvalidPrivateKey(privateKeyString)
+ }
+ var interface = InterfaceConfiguration(privateKey: privateKey)
+ if let listenPortString = attributes["listen_port"] {
+ guard let listenPort = UInt16(listenPortString) else {
+ throw ParseError.interfaceHasInvalidListenPort(listenPortString)
+ }
+ if listenPort != 0 {
+ interface.listenPort = listenPort
+ }
+ }
+ return interface
+ }
+
+ private static func collate(peerAttributes attributes: [String: String]) throws -> PeerConfiguration {
+ guard let publicKeyString = attributes["public_key"] else {
+ throw ParseError.peerHasNoPublicKey
+ }
+ guard let publicKey = PublicKey(hexKey: publicKeyString) else {
+ throw ParseError.peerHasInvalidPublicKey(publicKeyString)
+ }
+ var peer = PeerConfiguration(publicKey: publicKey)
+ if let preSharedKeyString = attributes["preshared_key"] {
+ guard let preSharedKey = PreSharedKey(hexKey: preSharedKeyString) else {
+ throw ParseError.peerHasInvalidPreSharedKey(preSharedKeyString)
+ }
+ // TODO(zx2c4): does the compiler optimize this away?
+ var accumulator: UInt8 = 0
+ for index in 0..<preSharedKey.rawValue.count {
+ accumulator |= preSharedKey.rawValue[index]
+ }
+ if accumulator != 0 {
+ peer.preSharedKey = preSharedKey
+ }
+ }
+ if let allowedIPsString = attributes["allowed_ip"] {
+ var allowedIPs = [IPAddressRange]()
+ for allowedIPString in allowedIPsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
+ guard let allowedIP = IPAddressRange(from: allowedIPString) else {
+ throw ParseError.peerHasInvalidAllowedIP(allowedIPString)
+ }
+ allowedIPs.append(allowedIP)
+ }
+ peer.allowedIPs = allowedIPs
+ }
+ if let endpointString = attributes["endpoint"] {
+ guard let endpoint = Endpoint(from: endpointString) else {
+ throw ParseError.peerHasInvalidEndpoint(endpointString)
+ }
+ peer.endpoint = endpoint
+ }
+ if let persistentKeepAliveString = attributes["persistent_keepalive_interval"] {
+ guard let persistentKeepAlive = UInt16(persistentKeepAliveString) else {
+ throw ParseError.peerHasInvalidPersistentKeepAlive(persistentKeepAliveString)
+ }
+ if persistentKeepAlive != 0 {
+ peer.persistentKeepAlive = persistentKeepAlive
+ }
+ }
+ if let rxBytesString = attributes["rx_bytes"] {
+ guard let rxBytes = UInt64(rxBytesString) else {
+ throw ParseError.peerHasInvalidTransferBytes(rxBytesString)
+ }
+ if rxBytes != 0 {
+ peer.rxBytes = rxBytes
+ }
+ }
+ if let txBytesString = attributes["tx_bytes"] {
+ guard let txBytes = UInt64(txBytesString) else {
+ throw ParseError.peerHasInvalidTransferBytes(txBytesString)
+ }
+ if txBytes != 0 {
+ peer.txBytes = txBytes
+ }
+ }
+ if let lastHandshakeTimeSecString = attributes["last_handshake_time_sec"] {
+ var lastHandshakeTimeSince1970: TimeInterval = 0
+ guard let lastHandshakeTimeSec = UInt64(lastHandshakeTimeSecString) else {
+ throw ParseError.peerHasInvalidLastHandshakeTime(lastHandshakeTimeSecString)
+ }
+ if lastHandshakeTimeSec != 0 {
+ lastHandshakeTimeSince1970 += Double(lastHandshakeTimeSec)
+ if let lastHandshakeTimeNsecString = attributes["last_handshake_time_nsec"] {
+ guard let lastHandshakeTimeNsec = UInt64(lastHandshakeTimeNsecString) else {
+ throw ParseError.peerHasInvalidLastHandshakeTime(lastHandshakeTimeNsecString)
+ }
+ lastHandshakeTimeSince1970 += Double(lastHandshakeTimeNsec) / 1000000000.0
+ }
+ peer.lastHandshakeTime = Date(timeIntervalSince1970: lastHandshakeTimeSince1970)
+ }
+ }
+ return peer
+ }
+}
diff --git a/Sources/WireGuardApp/Tunnel/TunnelErrors.swift b/Sources/WireGuardApp/Tunnel/TunnelErrors.swift
new file mode 100644
index 0000000..941ab61
--- /dev/null
+++ b/Sources/WireGuardApp/Tunnel/TunnelErrors.swift
@@ -0,0 +1,107 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import NetworkExtension
+
+enum TunnelsManagerError: WireGuardAppError {
+ case tunnelNameEmpty
+ case tunnelAlreadyExistsWithThatName
+ case systemErrorOnListingTunnels(systemError: Error)
+ case systemErrorOnAddTunnel(systemError: Error)
+ case systemErrorOnModifyTunnel(systemError: Error)
+ case systemErrorOnRemoveTunnel(systemError: Error)
+
+ var alertText: AlertText {
+ switch self {
+ case .tunnelNameEmpty:
+ return (tr("alertTunnelNameEmptyTitle"), tr("alertTunnelNameEmptyMessage"))
+ case .tunnelAlreadyExistsWithThatName:
+ return (tr("alertTunnelAlreadyExistsWithThatNameTitle"), tr("alertTunnelAlreadyExistsWithThatNameMessage"))
+ case .systemErrorOnListingTunnels(let systemError):
+ return (tr("alertSystemErrorOnListingTunnelsTitle"), systemError.localizedUIString)
+ case .systemErrorOnAddTunnel(let systemError):
+ return (tr("alertSystemErrorOnAddTunnelTitle"), systemError.localizedUIString)
+ case .systemErrorOnModifyTunnel(let systemError):
+ return (tr("alertSystemErrorOnModifyTunnelTitle"), systemError.localizedUIString)
+ case .systemErrorOnRemoveTunnel(let systemError):
+ return (tr("alertSystemErrorOnRemoveTunnelTitle"), systemError.localizedUIString)
+ }
+ }
+}
+
+enum TunnelsManagerActivationAttemptError: WireGuardAppError {
+ case tunnelIsNotInactive
+ case failedWhileStarting(systemError: Error) // startTunnel() throwed
+ case failedWhileSaving(systemError: Error) // save config after re-enabling throwed
+ case failedWhileLoading(systemError: Error) // reloading config throwed
+ case failedBecauseOfTooManyErrors(lastSystemError: Error) // recursion limit reached
+
+ var alertText: AlertText {
+ switch self {
+ case .tunnelIsNotInactive:
+ return (tr("alertTunnelActivationErrorTunnelIsNotInactiveTitle"), tr("alertTunnelActivationErrorTunnelIsNotInactiveMessage"))
+ case .failedWhileStarting(let systemError),
+ .failedWhileSaving(let systemError),
+ .failedWhileLoading(let systemError),
+ .failedBecauseOfTooManyErrors(let systemError):
+ return (tr("alertTunnelActivationSystemErrorTitle"),
+ tr(format: "alertTunnelActivationSystemErrorMessage (%@)", systemError.localizedUIString))
+ }
+ }
+}
+
+enum TunnelsManagerActivationError: WireGuardAppError {
+ case activationFailed(wasOnDemandEnabled: Bool)
+ case activationFailedWithExtensionError(title: String, message: String, wasOnDemandEnabled: Bool)
+
+ var alertText: AlertText {
+ switch self {
+ case .activationFailed(let wasOnDemandEnabled):
+ return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage") + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : ""))
+ case .activationFailedWithExtensionError(let title, let message, let wasOnDemandEnabled):
+ return (title, message + (wasOnDemandEnabled ? tr("alertTunnelActivationFailureOnDemandAddendum") : ""))
+ }
+ }
+}
+
+extension PacketTunnelProviderError: WireGuardAppError {
+ var alertText: AlertText {
+ switch self {
+ case .savedProtocolConfigurationIsInvalid:
+ return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationSavedConfigFailureMessage"))
+ case .dnsResolutionFailure:
+ return (tr("alertTunnelDNSFailureTitle"), tr("alertTunnelDNSFailureMessage"))
+ case .couldNotStartBackend:
+ return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationBackendFailureMessage"))
+ case .couldNotDetermineFileDescriptor:
+ return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFileDescriptorFailureMessage"))
+ case .couldNotSetNetworkSettings:
+ return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationSetNetworkSettingsMessage"))
+ }
+ }
+}
+
+extension Error {
+ var localizedUIString: String {
+ if let systemError = self as? NEVPNError {
+ switch systemError {
+ case NEVPNError.configurationInvalid:
+ return tr("alertSystemErrorMessageTunnelConfigurationInvalid")
+ case NEVPNError.configurationDisabled:
+ return tr("alertSystemErrorMessageTunnelConfigurationDisabled")
+ case NEVPNError.connectionFailed:
+ return tr("alertSystemErrorMessageTunnelConnectionFailed")
+ case NEVPNError.configurationStale:
+ return tr("alertSystemErrorMessageTunnelConfigurationStale")
+ case NEVPNError.configurationReadWriteFailed:
+ return tr("alertSystemErrorMessageTunnelConfigurationReadWriteFailed")
+ case NEVPNError.configurationUnknown:
+ return tr("alertSystemErrorMessageTunnelConfigurationUnknown")
+ default:
+ return ""
+ }
+ } else {
+ return localizedDescription
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/Tunnel/TunnelStatus.swift b/Sources/WireGuardApp/Tunnel/TunnelStatus.swift
new file mode 100644
index 0000000..547aa9f
--- /dev/null
+++ b/Sources/WireGuardApp/Tunnel/TunnelStatus.swift
@@ -0,0 +1,63 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+import NetworkExtension
+
+@objc enum TunnelStatus: Int {
+ case inactive
+ case activating
+ case active
+ case deactivating
+ case reasserting // Not a possible state at present
+ case restarting // Restarting tunnel (done after saving modifications to an active tunnel)
+ case waiting // Waiting for another tunnel to be brought down
+
+ init(from systemStatus: NEVPNStatus) {
+ switch systemStatus {
+ case .connected:
+ self = .active
+ case .connecting:
+ self = .activating
+ case .disconnected:
+ self = .inactive
+ case .disconnecting:
+ self = .deactivating
+ case .reasserting:
+ self = .reasserting
+ case .invalid:
+ self = .inactive
+ @unknown default:
+ fatalError()
+ }
+ }
+}
+
+extension TunnelStatus: CustomDebugStringConvertible {
+ public var debugDescription: String {
+ switch self {
+ case .inactive: return "inactive"
+ case .activating: return "activating"
+ case .active: return "active"
+ case .deactivating: return "deactivating"
+ case .reasserting: return "reasserting"
+ case .restarting: return "restarting"
+ case .waiting: return "waiting"
+ }
+ }
+}
+
+extension NEVPNStatus: CustomDebugStringConvertible {
+ public var debugDescription: String {
+ switch self {
+ case .connected: return "connected"
+ case .connecting: return "connecting"
+ case .disconnected: return "disconnected"
+ case .disconnecting: return "disconnecting"
+ case .reasserting: return "reasserting"
+ case .invalid: return "invalid"
+ @unknown default:
+ fatalError()
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/Tunnel/TunnelsManager.swift b/Sources/WireGuardApp/Tunnel/TunnelsManager.swift
new file mode 100644
index 0000000..af6a9bb
--- /dev/null
+++ b/Sources/WireGuardApp/Tunnel/TunnelsManager.swift
@@ -0,0 +1,807 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+import NetworkExtension
+import os.log
+import WireGuardKit
+
+protocol TunnelsManagerListDelegate: class {
+ func tunnelAdded(at index: Int)
+ func tunnelModified(at index: Int)
+ func tunnelMoved(from oldIndex: Int, to newIndex: Int)
+ func tunnelRemoved(at index: Int, tunnel: TunnelContainer)
+}
+
+protocol TunnelsManagerActivationDelegate: class {
+ func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) // startTunnel wasn't called or failed
+ func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) // startTunnel succeeded
+ func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) // status didn't change to connected
+ func tunnelActivationSucceeded(tunnel: TunnelContainer) // status changed to connected
+}
+
+class TunnelsManager {
+ fileprivate var tunnels: [TunnelContainer]
+ weak var tunnelsListDelegate: TunnelsManagerListDelegate?
+ weak var activationDelegate: TunnelsManagerActivationDelegate?
+ private var statusObservationToken: AnyObject?
+ private var waiteeObservationToken: AnyObject?
+ private var configurationsObservationToken: AnyObject?
+ private var catalinaWorkaround: Any?
+
+ init(tunnelProviders: [NETunnelProviderManager]) {
+ tunnels = tunnelProviders.map { TunnelContainer(tunnel: $0) }.sorted { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
+ startObservingTunnelStatuses()
+ startObservingTunnelConfigurations()
+ #if os(macOS)
+ if #available(macOS 10.15, *) {
+ self.catalinaWorkaround = CatalinaWorkaround(tunnelsManager: self)
+ }
+ #endif
+ }
+
+ static func create(completionHandler: @escaping (Result<TunnelsManager, TunnelsManagerError>) -> Void) {
+ #if targetEnvironment(simulator)
+ completionHandler(.success(TunnelsManager(tunnelProviders: MockTunnels.createMockTunnels())))
+ #else
+ NETunnelProviderManager.loadAllFromPreferences { managers, error in
+ if let error = error {
+ wg_log(.error, message: "Failed to load tunnel provider managers: \(error)")
+ completionHandler(.failure(TunnelsManagerError.systemErrorOnListingTunnels(systemError: error)))
+ return
+ }
+
+ var tunnelManagers = managers ?? []
+ var refs: Set<Data> = []
+ var tunnelNames: Set<String> = []
+ for (index, tunnelManager) in tunnelManagers.enumerated().reversed() {
+ if let tunnelName = tunnelManager.localizedDescription {
+ tunnelNames.insert(tunnelName)
+ }
+ guard let proto = tunnelManager.protocolConfiguration as? NETunnelProviderProtocol else { continue }
+ if proto.migrateConfigurationIfNeeded(called: tunnelManager.localizedDescription ?? "unknown") {
+ tunnelManager.saveToPreferences { _ in }
+ }
+ #if os(iOS)
+ let passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
+ #elseif os(macOS)
+ let passwordRef: Data?
+ if proto.providerConfiguration?["UID"] as? uid_t == getuid() {
+ passwordRef = proto.verifyConfigurationReference() ? proto.passwordReference : nil
+ } else {
+ passwordRef = proto.passwordReference // To handle multiple users in macOS, we skip verifying
+ }
+ #else
+ #error("Unimplemented")
+ #endif
+ if let ref = passwordRef {
+ refs.insert(ref)
+ } else {
+ wg_log(.info, message: "Removing orphaned tunnel with non-verifying keychain entry: \(tunnelManager.localizedDescription ?? "<unknown>")")
+ tunnelManager.removeFromPreferences { _ in }
+ tunnelManagers.remove(at: index)
+ }
+ }
+ #if os(macOS)
+ if #available(macOS 10.15, *) {
+ // Don't delete orphaned keychain refs. We need them to restore tunnels as a workaround.
+ } else {
+ Keychain.deleteReferences(except: refs)
+ }
+ #else
+ Keychain.deleteReferences(except: refs)
+ #endif
+ #if os(iOS)
+ RecentTunnelsTracker.cleanupTunnels(except: tunnelNames)
+ #endif
+ completionHandler(.success(TunnelsManager(tunnelProviders: tunnelManagers)))
+ }
+ #endif
+ }
+
+ func reload() {
+ NETunnelProviderManager.loadAllFromPreferences { [weak self] managers, _ in
+ guard let self = self else { return }
+
+ let loadedTunnelProviders = managers ?? []
+
+ for (index, currentTunnel) in self.tunnels.enumerated().reversed() {
+ if !loadedTunnelProviders.contains(where: { $0.isEquivalentTo(currentTunnel) }) {
+ // Tunnel was deleted outside the app
+ self.tunnels.remove(at: index)
+ self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: currentTunnel)
+ }
+ }
+ for loadedTunnelProvider in loadedTunnelProviders {
+ if let matchingTunnel = self.tunnels.first(where: { loadedTunnelProvider.isEquivalentTo($0) }) {
+ matchingTunnel.tunnelProvider = loadedTunnelProvider
+ matchingTunnel.refreshStatus()
+ } else {
+ // Tunnel was added outside the app
+ if let proto = loadedTunnelProvider.protocolConfiguration as? NETunnelProviderProtocol {
+ if proto.migrateConfigurationIfNeeded(called: loadedTunnelProvider.localizedDescription ?? "unknown") {
+ loadedTunnelProvider.saveToPreferences { _ in }
+ }
+ }
+ let tunnel = TunnelContainer(tunnel: loadedTunnelProvider)
+ self.tunnels.append(tunnel)
+ self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
+ self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
+ }
+ }
+ }
+ }
+
+ func add(tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption = .off, completionHandler: @escaping (Result<TunnelContainer, TunnelsManagerError>) -> Void) {
+ let tunnelName = tunnelConfiguration.name ?? ""
+ if tunnelName.isEmpty {
+ completionHandler(.failure(TunnelsManagerError.tunnelNameEmpty))
+ return
+ }
+
+ if tunnels.contains(where: { $0.name == tunnelName }) {
+ completionHandler(.failure(TunnelsManagerError.tunnelAlreadyExistsWithThatName))
+ return
+ }
+
+ let tunnelProviderManager = NETunnelProviderManager()
+ tunnelProviderManager.setTunnelConfiguration(tunnelConfiguration)
+ tunnelProviderManager.isEnabled = true
+
+ onDemandOption.apply(on: tunnelProviderManager)
+
+ let activeTunnel = tunnels.first { $0.status == .active || $0.status == .activating }
+
+ tunnelProviderManager.saveToPreferences { [weak self] error in
+ guard error == nil else {
+ wg_log(.error, message: "Add: Saving configuration failed: \(error!)")
+ (tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
+ completionHandler(.failure(TunnelsManagerError.systemErrorOnAddTunnel(systemError: error!)))
+ return
+ }
+
+ guard let self = self else { return }
+
+ #if os(iOS)
+ // HACK: In iOS, adding a tunnel causes deactivation of any currently active tunnel.
+ // This is an ugly hack to reactivate the tunnel that has been deactivated like that.
+ if let activeTunnel = activeTunnel {
+ if activeTunnel.status == .inactive || activeTunnel.status == .deactivating {
+ self.startActivation(of: activeTunnel)
+ }
+ if activeTunnel.status == .active || activeTunnel.status == .activating {
+ activeTunnel.status = .restarting
+ }
+ }
+ #endif
+
+ let tunnel = TunnelContainer(tunnel: tunnelProviderManager)
+ self.tunnels.append(tunnel)
+ self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
+ self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
+ completionHandler(.success(tunnel))
+ }
+ }
+
+ func addMultiple(tunnelConfigurations: [TunnelConfiguration], completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
+ addMultiple(tunnelConfigurations: ArraySlice(tunnelConfigurations), numberSuccessful: 0, lastError: nil, completionHandler: completionHandler)
+ }
+
+ private func addMultiple(tunnelConfigurations: ArraySlice<TunnelConfiguration>, numberSuccessful: UInt, lastError: TunnelsManagerError?, completionHandler: @escaping (UInt, TunnelsManagerError?) -> Void) {
+ guard let head = tunnelConfigurations.first else {
+ completionHandler(numberSuccessful, lastError)
+ return
+ }
+ let tail = tunnelConfigurations.dropFirst()
+ add(tunnelConfiguration: head) { [weak self, tail] result in
+ DispatchQueue.main.async {
+ var numberSuccessfulCount = numberSuccessful
+ var lastError: TunnelsManagerError?
+ switch result {
+ case .failure(let error):
+ lastError = error
+ case .success:
+ numberSuccessfulCount = numberSuccessful + 1
+ }
+ self?.addMultiple(tunnelConfigurations: tail, numberSuccessful: numberSuccessfulCount, lastError: lastError, completionHandler: completionHandler)
+ }
+ }
+ }
+
+ func modify(tunnel: TunnelContainer, tunnelConfiguration: TunnelConfiguration, onDemandOption: ActivateOnDemandOption, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
+ let tunnelName = tunnelConfiguration.name ?? ""
+ if tunnelName.isEmpty {
+ completionHandler(TunnelsManagerError.tunnelNameEmpty)
+ return
+ }
+
+ let tunnelProviderManager = tunnel.tunnelProvider
+ let oldName = tunnelProviderManager.localizedDescription ?? ""
+ let isNameChanged = tunnelName != oldName
+ if isNameChanged {
+ guard !tunnels.contains(where: { $0.name == tunnelName }) else {
+ completionHandler(TunnelsManagerError.tunnelAlreadyExistsWithThatName)
+ return
+ }
+ tunnel.name = tunnelName
+ }
+
+ var isTunnelConfigurationChanged = false
+ if tunnelProviderManager.tunnelConfiguration != tunnelConfiguration {
+ tunnelProviderManager.setTunnelConfiguration(tunnelConfiguration)
+ isTunnelConfigurationChanged = true
+ }
+ tunnelProviderManager.isEnabled = true
+
+ let isActivatingOnDemand = !tunnelProviderManager.isOnDemandEnabled && onDemandOption != .off
+ onDemandOption.apply(on: tunnelProviderManager)
+
+ tunnelProviderManager.saveToPreferences { [weak self] error in
+ guard error == nil else {
+ //TODO: the passwordReference for the old one has already been removed at this point and we can't easily roll back!
+ wg_log(.error, message: "Modify: Saving configuration failed: \(error!)")
+ completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!))
+ return
+ }
+ guard let self = self else { return }
+ if isNameChanged {
+ let oldIndex = self.tunnels.firstIndex(of: tunnel)!
+ self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
+ let newIndex = self.tunnels.firstIndex(of: tunnel)!
+ self.tunnelsListDelegate?.tunnelMoved(from: oldIndex, to: newIndex)
+ #if os(iOS)
+ RecentTunnelsTracker.handleTunnelRenamed(oldName: oldName, newName: tunnelName)
+ #endif
+ }
+ self.tunnelsListDelegate?.tunnelModified(at: self.tunnels.firstIndex(of: tunnel)!)
+
+ if isTunnelConfigurationChanged {
+ if tunnel.status == .active || tunnel.status == .activating || tunnel.status == .reasserting {
+ // Turn off the tunnel, and then turn it back on, so the changes are made effective
+ tunnel.status = .restarting
+ (tunnel.tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
+ }
+ }
+
+ if isActivatingOnDemand {
+ // Reload tunnel after saving.
+ // Without this, the tunnel stopes getting updates on the tunnel status from iOS.
+ tunnelProviderManager.loadFromPreferences { error in
+ tunnel.isActivateOnDemandEnabled = tunnelProviderManager.isOnDemandEnabled
+ guard error == nil else {
+ wg_log(.error, message: "Modify: Re-loading after saving configuration failed: \(error!)")
+ completionHandler(TunnelsManagerError.systemErrorOnModifyTunnel(systemError: error!))
+ return
+ }
+ completionHandler(nil)
+ }
+ } else {
+ completionHandler(nil)
+ }
+ }
+ }
+
+ func remove(tunnel: TunnelContainer, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
+ let tunnelProviderManager = tunnel.tunnelProvider
+ #if os(macOS)
+ if tunnel.isTunnelAvailableToUser {
+ (tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
+ }
+ #elseif os(iOS)
+ (tunnelProviderManager.protocolConfiguration as? NETunnelProviderProtocol)?.destroyConfigurationReference()
+ #else
+ #error("Unimplemented")
+ #endif
+ tunnelProviderManager.removeFromPreferences { [weak self] error in
+ guard error == nil else {
+ wg_log(.error, message: "Remove: Saving configuration failed: \(error!)")
+ completionHandler(TunnelsManagerError.systemErrorOnRemoveTunnel(systemError: error!))
+ return
+ }
+ if let self = self, let index = self.tunnels.firstIndex(of: tunnel) {
+ self.tunnels.remove(at: index)
+ self.tunnelsListDelegate?.tunnelRemoved(at: index, tunnel: tunnel)
+ }
+ completionHandler(nil)
+
+ #if os(iOS)
+ RecentTunnelsTracker.handleTunnelRemoved(tunnelName: tunnel.name)
+ #endif
+ }
+ }
+
+ func removeMultiple(tunnels: [TunnelContainer], completionHandler: @escaping (TunnelsManagerError?) -> Void) {
+ removeMultiple(tunnels: ArraySlice(tunnels), completionHandler: completionHandler)
+ }
+
+ private func removeMultiple(tunnels: ArraySlice<TunnelContainer>, completionHandler: @escaping (TunnelsManagerError?) -> Void) {
+ guard let head = tunnels.first else {
+ completionHandler(nil)
+ return
+ }
+ let tail = tunnels.dropFirst()
+ remove(tunnel: head) { [weak self, tail] error in
+ DispatchQueue.main.async {
+ if let error = error {
+ completionHandler(error)
+ } else {
+ self?.removeMultiple(tunnels: tail, completionHandler: completionHandler)
+ }
+ }
+ }
+ }
+
+ func numberOfTunnels() -> Int {
+ return tunnels.count
+ }
+
+ func tunnel(at index: Int) -> TunnelContainer {
+ return tunnels[index]
+ }
+
+ func mapTunnels<T>(transform: (TunnelContainer) throws -> T) rethrows -> [T] {
+ return try tunnels.map(transform)
+ }
+
+ func index(of tunnel: TunnelContainer) -> Int? {
+ return tunnels.firstIndex(of: tunnel)
+ }
+
+ func tunnel(named tunnelName: String) -> TunnelContainer? {
+ return tunnels.first { $0.name == tunnelName }
+ }
+
+ func waitingTunnel() -> TunnelContainer? {
+ return tunnels.first { $0.status == .waiting }
+ }
+
+ func tunnelInOperation() -> TunnelContainer? {
+ if let waitingTunnelObject = waitingTunnel() {
+ return waitingTunnelObject
+ }
+ return tunnels.first { $0.status != .inactive }
+ }
+
+ func startActivation(of tunnel: TunnelContainer) {
+ guard tunnels.contains(tunnel) else { return } // Ensure it's not deleted
+ guard tunnel.status == .inactive else {
+ activationDelegate?.tunnelActivationAttemptFailed(tunnel: tunnel, error: .tunnelIsNotInactive)
+ return
+ }
+
+ if let alreadyWaitingTunnel = tunnels.first(where: { $0.status == .waiting }) {
+ alreadyWaitingTunnel.status = .inactive
+ }
+
+ if let tunnelInOperation = tunnels.first(where: { $0.status != .inactive }) {
+ wg_log(.info, message: "Tunnel '\(tunnel.name)' waiting for deactivation of '\(tunnelInOperation.name)'")
+ tunnel.status = .waiting
+ activateWaitingTunnelOnDeactivation(of: tunnelInOperation)
+ if tunnelInOperation.status != .deactivating {
+ startDeactivation(of: tunnelInOperation)
+ }
+ return
+ }
+
+ #if targetEnvironment(simulator)
+ tunnel.status = .active
+ #else
+ tunnel.startActivation(activationDelegate: activationDelegate)
+ #endif
+
+ #if os(iOS)
+ RecentTunnelsTracker.handleTunnelActivated(tunnelName: tunnel.name)
+ #endif
+ }
+
+ func startDeactivation(of tunnel: TunnelContainer) {
+ tunnel.isAttemptingActivation = false
+ guard tunnel.status != .inactive && tunnel.status != .deactivating else { return }
+ #if targetEnvironment(simulator)
+ tunnel.status = .inactive
+ #else
+ tunnel.startDeactivation()
+ #endif
+ }
+
+ func refreshStatuses() {
+ tunnels.forEach { $0.refreshStatus() }
+ }
+
+ private func activateWaitingTunnelOnDeactivation(of tunnel: TunnelContainer) {
+ waiteeObservationToken = tunnel.observe(\.status) { [weak self] tunnel, _ in
+ guard let self = self else { return }
+ if tunnel.status == .inactive {
+ if let waitingTunnel = self.tunnels.first(where: { $0.status == .waiting }) {
+ waitingTunnel.startActivation(activationDelegate: self.activationDelegate)
+ }
+ self.waiteeObservationToken = nil
+ }
+ }
+ }
+
+ private func startObservingTunnelStatuses() {
+ statusObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNStatusDidChange, object: nil, queue: OperationQueue.main) { [weak self] statusChangeNotification in
+ guard let self = self,
+ let session = statusChangeNotification.object as? NETunnelProviderSession,
+ let tunnelProvider = session.manager as? NETunnelProviderManager,
+ let tunnel = self.tunnels.first(where: { $0.tunnelProvider == tunnelProvider }) else { return }
+
+ wg_log(.debug, message: "Tunnel '\(tunnel.name)' connection status changed to '\(tunnel.tunnelProvider.connection.status)'")
+
+ if tunnel.isAttemptingActivation {
+ if session.status == .connected {
+ tunnel.isAttemptingActivation = false
+ self.activationDelegate?.tunnelActivationSucceeded(tunnel: tunnel)
+ } else if session.status == .disconnected {
+ tunnel.isAttemptingActivation = false
+ if let (title, message) = lastErrorTextFromNetworkExtension(for: tunnel) {
+ self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailedWithExtensionError(title: title, message: message, wasOnDemandEnabled: tunnelProvider.isOnDemandEnabled))
+ } else {
+ self.activationDelegate?.tunnelActivationFailed(tunnel: tunnel, error: .activationFailed(wasOnDemandEnabled: tunnelProvider.isOnDemandEnabled))
+ }
+ }
+ }
+
+ if tunnel.status == .restarting && session.status == .disconnected {
+ tunnel.startActivation(activationDelegate: self.activationDelegate)
+ return
+ }
+
+ tunnel.refreshStatus()
+ }
+ }
+
+ func startObservingTunnelConfigurations() {
+ configurationsObservationToken = NotificationCenter.default.addObserver(forName: .NEVPNConfigurationChange, object: nil, queue: OperationQueue.main) { [weak self] _ in
+ DispatchQueue.main.async { [weak self] in
+ // We schedule reload() in a subsequent runloop to ensure that the completion handler of loadAllFromPreferences
+ // (reload() calls loadAllFromPreferences) is called after the completion handler of the saveToPreferences or
+ // removeFromPreferences call, if any, that caused this notification to fire. This notification can also fire
+ // as a result of a tunnel getting added or removed outside of the app.
+ self?.reload()
+ }
+ }
+ }
+
+ static func tunnelNameIsLessThan(_ a: String, _ b: String) -> Bool {
+ return a.compare(b, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive, .numeric]) == .orderedAscending
+ }
+}
+
+private func lastErrorTextFromNetworkExtension(for tunnel: TunnelContainer) -> (title: String, message: String)? {
+ guard let lastErrorFileURL = FileManager.networkExtensionLastErrorFileURL else { return nil }
+ guard let lastErrorData = try? Data(contentsOf: lastErrorFileURL) else { return nil }
+ guard let lastErrorStrings = String(data: lastErrorData, encoding: .utf8)?.splitToArray(separator: "\n") else { return nil }
+ guard lastErrorStrings.count == 2 && tunnel.activationAttemptId == lastErrorStrings[0] else { return nil }
+
+ if let extensionError = PacketTunnelProviderError(rawValue: lastErrorStrings[1]) {
+ return extensionError.alertText
+ }
+
+ return (tr("alertTunnelActivationFailureTitle"), tr("alertTunnelActivationFailureMessage"))
+}
+
+class TunnelContainer: NSObject {
+ @objc dynamic var name: String
+ @objc dynamic var status: TunnelStatus
+
+ @objc dynamic var isActivateOnDemandEnabled: Bool
+
+ var isAttemptingActivation = false {
+ didSet {
+ if isAttemptingActivation {
+ self.activationTimer?.invalidate()
+ let activationTimer = Timer(timeInterval: 5 /* seconds */, repeats: true) { [weak self] _ in
+ guard let self = self else { return }
+ wg_log(.debug, message: "Status update notification timeout for tunnel '\(self.name)'. Tunnel status is now '\(self.tunnelProvider.connection.status)'.")
+ switch self.tunnelProvider.connection.status {
+ case .connected, .disconnected, .invalid:
+ self.activationTimer?.invalidate()
+ self.activationTimer = nil
+ default:
+ break
+ }
+ self.refreshStatus()
+ }
+ self.activationTimer = activationTimer
+ RunLoop.main.add(activationTimer, forMode: .common)
+ }
+ }
+ }
+ var activationAttemptId: String?
+ var activationTimer: Timer?
+ var deactivationTimer: Timer?
+
+ fileprivate var tunnelProvider: NETunnelProviderManager
+
+ var tunnelConfiguration: TunnelConfiguration? {
+ return tunnelProvider.tunnelConfiguration
+ }
+
+ var onDemandOption: ActivateOnDemandOption {
+ return ActivateOnDemandOption(from: tunnelProvider)
+ }
+
+ #if os(macOS)
+ var isTunnelAvailableToUser: Bool {
+ return (tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol)?.providerConfiguration?["UID"] as? uid_t == getuid()
+ }
+ #endif
+
+ init(tunnel: NETunnelProviderManager) {
+ name = tunnel.localizedDescription ?? "Unnamed"
+ let status = TunnelStatus(from: tunnel.connection.status)
+ self.status = status
+ isActivateOnDemandEnabled = tunnel.isOnDemandEnabled
+ tunnelProvider = tunnel
+ super.init()
+ }
+
+ func getRuntimeTunnelConfiguration(completionHandler: @escaping ((TunnelConfiguration?) -> Void)) {
+ guard status != .inactive, let session = tunnelProvider.connection as? NETunnelProviderSession else {
+ completionHandler(tunnelConfiguration)
+ return
+ }
+ guard nil != (try? session.sendProviderMessage(Data([ UInt8(0) ]), responseHandler: {
+ guard self.status != .inactive, let data = $0, let base = self.tunnelConfiguration, let settings = String(data: data, encoding: .utf8) else {
+ completionHandler(self.tunnelConfiguration)
+ return
+ }
+ completionHandler((try? TunnelConfiguration(fromUapiConfig: settings, basedOn: base)) ?? self.tunnelConfiguration)
+ })) else {
+ completionHandler(tunnelConfiguration)
+ return
+ }
+ }
+
+ func refreshStatus() {
+ if status == .restarting {
+ return
+ }
+ status = TunnelStatus(from: tunnelProvider.connection.status)
+ isActivateOnDemandEnabled = tunnelProvider.isOnDemandEnabled
+ }
+
+ fileprivate func startActivation(recursionCount: UInt = 0, lastError: Error? = nil, activationDelegate: TunnelsManagerActivationDelegate?) {
+ if recursionCount >= 8 {
+ wg_log(.error, message: "startActivation: Failed after 8 attempts. Giving up with \(lastError!)")
+ activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedBecauseOfTooManyErrors(lastSystemError: lastError!))
+ return
+ }
+
+ wg_log(.debug, message: "startActivation: Entering (tunnel: \(name))")
+
+ status = .activating // Ensure that no other tunnel can attempt activation until this tunnel is done trying
+
+ guard tunnelProvider.isEnabled else {
+ // In case the tunnel had gotten disabled, re-enable and save it,
+ // then call this function again.
+ wg_log(.debug, staticMessage: "startActivation: Tunnel is disabled. Re-enabling and saving")
+ tunnelProvider.isEnabled = true
+ tunnelProvider.saveToPreferences { [weak self] error in
+ guard let self = self else { return }
+ if error != nil {
+ wg_log(.error, message: "Error saving tunnel after re-enabling: \(error!)")
+ activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileSaving(systemError: error!))
+ return
+ }
+ wg_log(.debug, staticMessage: "startActivation: Tunnel saved after re-enabling, invoking startActivation")
+ self.startActivation(recursionCount: recursionCount + 1, lastError: NEVPNError(NEVPNError.configurationUnknown), activationDelegate: activationDelegate)
+ }
+ return
+ }
+
+ // Start the tunnel
+ do {
+ wg_log(.debug, staticMessage: "startActivation: Starting tunnel")
+ isAttemptingActivation = true
+ let activationAttemptId = UUID().uuidString
+ self.activationAttemptId = activationAttemptId
+ try (tunnelProvider.connection as? NETunnelProviderSession)?.startTunnel(options: ["activationAttemptId": activationAttemptId])
+ wg_log(.debug, staticMessage: "startActivation: Success")
+ activationDelegate?.tunnelActivationAttemptSucceeded(tunnel: self)
+ } catch let error {
+ isAttemptingActivation = false
+ guard let systemError = error as? NEVPNError else {
+ wg_log(.error, message: "Failed to activate tunnel: Error: \(error)")
+ status = .inactive
+ activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting(systemError: error))
+ return
+ }
+ guard systemError.code == NEVPNError.configurationInvalid || systemError.code == NEVPNError.configurationStale else {
+ wg_log(.error, message: "Failed to activate tunnel: VPN Error: \(error)")
+ status = .inactive
+ activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileStarting(systemError: systemError))
+ return
+ }
+ wg_log(.debug, staticMessage: "startActivation: Will reload tunnel and then try to start it.")
+ tunnelProvider.loadFromPreferences { [weak self] error in
+ guard let self = self else { return }
+ if error != nil {
+ wg_log(.error, message: "startActivation: Error reloading tunnel: \(error!)")
+ self.status = .inactive
+ activationDelegate?.tunnelActivationAttemptFailed(tunnel: self, error: .failedWhileLoading(systemError: systemError))
+ return
+ }
+ wg_log(.debug, staticMessage: "startActivation: Tunnel reloaded, invoking startActivation")
+ self.startActivation(recursionCount: recursionCount + 1, lastError: systemError, activationDelegate: activationDelegate)
+ }
+ }
+ }
+
+ fileprivate func startDeactivation() {
+ wg_log(.debug, message: "startDeactivation: Tunnel: \(name)")
+ (tunnelProvider.connection as? NETunnelProviderSession)?.stopTunnel()
+ }
+}
+
+extension NETunnelProviderManager {
+ fileprivate static var cachedConfigKey: UInt8 = 0
+
+ var tunnelConfiguration: TunnelConfiguration? {
+ if let cached = objc_getAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey) as? TunnelConfiguration {
+ return cached
+ }
+ let config = (protocolConfiguration as? NETunnelProviderProtocol)?.asTunnelConfiguration(called: localizedDescription)
+ if config != nil {
+ objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, config, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+ }
+ return config
+ }
+
+ func setTunnelConfiguration(_ tunnelConfiguration: TunnelConfiguration) {
+ protocolConfiguration = NETunnelProviderProtocol(tunnelConfiguration: tunnelConfiguration, previouslyFrom: protocolConfiguration)
+ localizedDescription = tunnelConfiguration.name
+ objc_setAssociatedObject(self, &NETunnelProviderManager.cachedConfigKey, tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+ }
+
+ func isEquivalentTo(_ tunnel: TunnelContainer) -> Bool {
+ return localizedDescription == tunnel.name && tunnelConfiguration == tunnel.tunnelConfiguration
+ }
+}
+
+#if os(macOS)
+@available(macOS 10.15, *)
+class CatalinaWorkaround {
+
+ // In macOS Catalina, for some users, the tunnels get deleted arbitrarily
+ // by the OS. It's not clear what triggers that.
+
+ // As a workaround, in macOS Catalina, when we realize that tunnels have been
+ // deleted outside the app, we reinstate those tunnels using the information
+ // in the keychain.
+
+ unowned let tunnelsManager: TunnelsManager
+ private var configChangeSubscriber: Any?
+
+ struct ReinstationData {
+ let tunnelConfiguration: TunnelConfiguration
+ let keychainPasswordRef: Data
+ }
+
+ init(tunnelsManager: TunnelsManager) {
+ self.tunnelsManager = tunnelsManager
+
+ // Attempt reinstation when there's a change in tunnel configurations,
+ // which indicates that tunnels may have been deleted outside the app.
+ // We use debounce to wait for all change notifications to arrive
+ // before attempting to reinstate, so that we don't have saveToPreferences
+ // being called while another saveToPreferences is in progress.
+ self.configChangeSubscriber = NotificationCenter.default
+ .publisher(for: .NEVPNConfigurationChange, object: nil)
+ .debounce(for: .seconds(1), scheduler: RunLoop.main)
+ .subscribe(on: RunLoop.main)
+ .sink { [weak self] _ in
+ self?.reinstateTunnelsDeletedOutsideApp()
+ }
+
+ // Attempt reinstation on app launch
+ reinstateTunnelsDeletedOutsideApp()
+ }
+
+ func reinstateTunnelsDeletedOutsideApp() {
+ let rd = reinstationDataForTunnelsDeletedOutsideApp()
+ reinstateTunnels(ArraySlice(rd), completionHandler: nil)
+ }
+
+ private func reinstateTunnels(_ rdArray: ArraySlice<ReinstationData>, completionHandler: (() -> Void)?) {
+ guard let head = rdArray.first else {
+ completionHandler?()
+ return
+ }
+ let tail = rdArray.dropFirst()
+ self.tunnelsManager.reinstateTunnel(reinstationData: head) { _ in
+ DispatchQueue.main.async {
+ self.reinstateTunnels(tail, completionHandler: completionHandler)
+ }
+ }
+ }
+
+ private func reinstationDataForTunnelsDeletedOutsideApp() -> [ReinstationData] {
+ let knownRefs: [Data] = self.tunnelsManager.tunnels
+ .compactMap { $0.tunnelProvider.protocolConfiguration as? NETunnelProviderProtocol }
+ .compactMap { $0.passwordReference }
+ let knownRefsSet: Set<Data> = Set(knownRefs)
+ var result: CFTypeRef?
+ let ret = SecItemCopyMatching([kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: Bundle.main.bundleIdentifier as Any,
+ kSecMatchLimit as String: kSecMatchLimitAll,
+ kSecReturnAttributes as String: true,
+ kSecReturnPersistentRef as String: true] as CFDictionary,
+ &result)
+ guard ret == errSecSuccess, let resultDicts = result as? [[String: Any]] else { return [] }
+ let labelPrefix = "WireGuard Tunnel: "
+ var reinstationData: [ReinstationData] = []
+ for resultDict in resultDicts {
+ guard let ref = resultDict[kSecValuePersistentRef as String] as? Data else { continue }
+ guard let label = resultDict[kSecAttrLabel as String] as? String else { continue }
+ guard label.hasPrefix(labelPrefix) else { continue }
+ if !knownRefsSet.contains(ref) {
+ let tunnelName = String(label.dropFirst(labelPrefix.count))
+ if let configStr = Keychain.openReference(called: ref),
+ let config = try? TunnelConfiguration(fromWgQuickConfig: configStr, called: tunnelName) {
+ reinstationData.append(ReinstationData(tunnelConfiguration: config, keychainPasswordRef: ref))
+ }
+ }
+ }
+ return reinstationData
+ }
+}
+#endif
+
+#if os(macOS)
+@available(macOS 10.15, *)
+extension TunnelsManager {
+ fileprivate func reinstateTunnel(reinstationData: CatalinaWorkaround.ReinstationData, completionHandler: @escaping (Bool) -> Void) {
+ let tunnelName = reinstationData.tunnelConfiguration.name ?? ""
+ if tunnelName.isEmpty {
+ completionHandler(false)
+ return
+ }
+
+ if tunnels.contains(where: { $0.name == tunnelName }) {
+ completionHandler(false)
+ return
+ }
+
+ let tunnelProviderProtocol = NETunnelProviderProtocol()
+ guard let appId = Bundle.main.bundleIdentifier else { fatalError() }
+ tunnelProviderProtocol.providerBundleIdentifier = "\(appId).network-extension"
+ tunnelProviderProtocol.passwordReference = reinstationData.keychainPasswordRef
+ tunnelProviderProtocol.providerConfiguration = ["UID": getuid()]
+ tunnelProviderProtocol.serverAddress = {
+ let endpoints = reinstationData.tunnelConfiguration.peers.compactMap { $0.endpoint }
+ if endpoints.count == 1 {
+ return endpoints[0].stringRepresentation
+ } else if endpoints.isEmpty {
+ return "Unspecified"
+ } else {
+ return "Multiple endpoints"
+ }
+ }()
+
+ let tunnelProvider = NETunnelProviderManager()
+ tunnelProvider.localizedDescription = tunnelName
+ tunnelProvider.protocolConfiguration = tunnelProviderProtocol
+ objc_setAssociatedObject(tunnelProvider, &NETunnelProviderManager.cachedConfigKey, reinstationData.tunnelConfiguration, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+ tunnelProvider.isEnabled = true
+
+ tunnelProvider.saveToPreferences { [weak self] error in
+ guard error == nil else {
+ wg_log(.error, message: "Reinstate: Saving configuration failed: \(error!)")
+ completionHandler(false)
+ return
+ }
+
+ guard let self = self else { return }
+
+ let tunnel = TunnelContainer(tunnel: tunnelProvider)
+ self.tunnels.append(tunnel)
+ self.tunnels.sort { TunnelsManager.tunnelNameIsLessThan($0.name, $1.name) }
+ self.tunnelsListDelegate?.tunnelAdded(at: self.tunnels.firstIndex(of: tunnel)!)
+ completionHandler(true)
+ }
+ }
+}
+#endif
diff --git a/Sources/WireGuardApp/UI/ActivateOnDemandViewModel.swift b/Sources/WireGuardApp/UI/ActivateOnDemandViewModel.swift
new file mode 100644
index 0000000..55b9be2
--- /dev/null
+++ b/Sources/WireGuardApp/UI/ActivateOnDemandViewModel.swift
@@ -0,0 +1,198 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+
+class ActivateOnDemandViewModel {
+ enum OnDemandField {
+ case onDemand
+ case nonWiFiInterface
+ case wiFiInterface
+ case ssid
+
+ var localizedUIString: String {
+ switch self {
+ case .onDemand:
+ return tr("tunnelOnDemandKey")
+ case .nonWiFiInterface:
+ #if os(iOS)
+ return tr("tunnelOnDemandCellular")
+ #elseif os(macOS)
+ return tr("tunnelOnDemandEthernet")
+ #else
+ #error("Unimplemented")
+ #endif
+ case .wiFiInterface: return tr("tunnelOnDemandWiFi")
+ case .ssid: return tr("tunnelOnDemandSSIDsKey")
+ }
+ }
+ }
+
+ enum OnDemandSSIDOption {
+ case anySSID
+ case onlySpecificSSIDs
+ case exceptSpecificSSIDs
+
+ var localizedUIString: String {
+ switch self {
+ case .anySSID: return tr("tunnelOnDemandAnySSID")
+ case .onlySpecificSSIDs: return tr("tunnelOnDemandOnlyTheseSSIDs")
+ case .exceptSpecificSSIDs: return tr("tunnelOnDemandExceptTheseSSIDs")
+ }
+ }
+ }
+
+ var isNonWiFiInterfaceEnabled = false
+ var isWiFiInterfaceEnabled = false
+ var selectedSSIDs = [String]()
+ var ssidOption: OnDemandSSIDOption = .anySSID
+}
+
+extension ActivateOnDemandViewModel {
+ convenience init(tunnel: TunnelContainer) {
+ self.init()
+ if tunnel.isActivateOnDemandEnabled {
+ switch tunnel.onDemandOption {
+ case .off:
+ break
+ case .wiFiInterfaceOnly(let onDemandSSIDOption):
+ isWiFiInterfaceEnabled = true
+ (ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
+ case .nonWiFiInterfaceOnly:
+ isNonWiFiInterfaceEnabled = true
+ case .anyInterface(let onDemandSSIDOption):
+ isWiFiInterfaceEnabled = true
+ isNonWiFiInterfaceEnabled = true
+ (ssidOption, selectedSSIDs) = ssidViewModel(from: onDemandSSIDOption)
+ }
+ }
+ }
+
+ func toOnDemandOption() -> ActivateOnDemandOption {
+ switch (isWiFiInterfaceEnabled, isNonWiFiInterfaceEnabled) {
+ case (false, false):
+ return .off
+ case (false, true):
+ return .nonWiFiInterfaceOnly
+ case (true, false):
+ return .wiFiInterfaceOnly(toSSIDOption())
+ case (true, true):
+ return .anyInterface(toSSIDOption())
+ }
+ }
+}
+
+extension ActivateOnDemandViewModel {
+ func isEnabled(field: OnDemandField) -> Bool {
+ switch field {
+ case .nonWiFiInterface:
+ return isNonWiFiInterfaceEnabled
+ case .wiFiInterface:
+ return isWiFiInterfaceEnabled
+ default:
+ return false
+ }
+ }
+
+ func setEnabled(field: OnDemandField, isEnabled: Bool) {
+ switch field {
+ case .nonWiFiInterface:
+ isNonWiFiInterfaceEnabled = isEnabled
+ case .wiFiInterface:
+ isWiFiInterfaceEnabled = isEnabled
+ default:
+ break
+ }
+ }
+}
+
+extension ActivateOnDemandViewModel {
+ var localizedInterfaceDescription: String {
+ switch (isWiFiInterfaceEnabled, isNonWiFiInterfaceEnabled) {
+ case (false, false):
+ return tr("tunnelOnDemandOptionOff")
+ case (true, false):
+ return tr("tunnelOnDemandOptionWiFiOnly")
+ case (false, true):
+ #if os(iOS)
+ return tr("tunnelOnDemandOptionCellularOnly")
+ #elseif os(macOS)
+ return tr("tunnelOnDemandOptionEthernetOnly")
+ #else
+ #error("Unimplemented")
+ #endif
+ case (true, true):
+ #if os(iOS)
+ return tr("tunnelOnDemandOptionWiFiOrCellular")
+ #elseif os(macOS)
+ return tr("tunnelOnDemandOptionWiFiOrEthernet")
+ #else
+ #error("Unimplemented")
+ #endif
+ }
+ }
+
+ var localizedSSIDDescription: String {
+ guard isWiFiInterfaceEnabled else { return "" }
+ switch ssidOption {
+ case .anySSID: return tr("tunnelOnDemandAnySSID")
+ case .onlySpecificSSIDs:
+ if selectedSSIDs.count == 1 {
+ return tr(format: "tunnelOnDemandOnlySSID (%d)", selectedSSIDs.count)
+ } else {
+ return tr(format: "tunnelOnDemandOnlySSIDs (%d)", selectedSSIDs.count)
+ }
+ case .exceptSpecificSSIDs:
+ if selectedSSIDs.count == 1 {
+ return tr(format: "tunnelOnDemandExceptSSID (%d)", selectedSSIDs.count)
+ } else {
+ return tr(format: "tunnelOnDemandExceptSSIDs (%d)", selectedSSIDs.count)
+ }
+ }
+ }
+
+ func fixSSIDOption() {
+ selectedSSIDs = uniquifiedNonEmptySelectedSSIDs()
+ if selectedSSIDs.isEmpty {
+ ssidOption = .anySSID
+ }
+ }
+}
+
+private extension ActivateOnDemandViewModel {
+ func ssidViewModel(from ssidOption: ActivateOnDemandSSIDOption) -> (OnDemandSSIDOption, [String]) {
+ switch ssidOption {
+ case .anySSID:
+ return (.anySSID, [])
+ case .onlySpecificSSIDs(let ssids):
+ return (.onlySpecificSSIDs, ssids)
+ case .exceptSpecificSSIDs(let ssids):
+ return (.exceptSpecificSSIDs, ssids)
+ }
+ }
+
+ func toSSIDOption() -> ActivateOnDemandSSIDOption {
+ switch ssidOption {
+ case .anySSID:
+ return .anySSID
+ case .onlySpecificSSIDs:
+ let ssids = uniquifiedNonEmptySelectedSSIDs()
+ return ssids.isEmpty ? .anySSID : .onlySpecificSSIDs(selectedSSIDs)
+ case .exceptSpecificSSIDs:
+ let ssids = uniquifiedNonEmptySelectedSSIDs()
+ return ssids.isEmpty ? .anySSID : .exceptSpecificSSIDs(selectedSSIDs)
+ }
+ }
+
+ func uniquifiedNonEmptySelectedSSIDs() -> [String] {
+ let nonEmptySSIDs = selectedSSIDs.filter { !$0.isEmpty }
+ var seenSSIDs = Set<String>()
+ var uniquified = [String]()
+ for ssid in nonEmptySSIDs {
+ guard !seenSSIDs.contains(ssid) else { continue }
+ uniquified.append(ssid)
+ seenSSIDs.insert(ssid)
+ }
+ return uniquified
+ }
+}
diff --git a/Sources/WireGuardApp/UI/ErrorPresenterProtocol.swift b/Sources/WireGuardApp/UI/ErrorPresenterProtocol.swift
new file mode 100644
index 0000000..ee4cf48
--- /dev/null
+++ b/Sources/WireGuardApp/UI/ErrorPresenterProtocol.swift
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+protocol ErrorPresenterProtocol {
+ static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?, onDismissal: (() -> Void)?)
+}
+
+extension ErrorPresenterProtocol {
+ static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?) {
+ showErrorAlert(title: title, message: message, from: sourceVC, onPresented: onPresented, onDismissal: nil)
+ }
+
+ static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onDismissal: (() -> Void)?) {
+ showErrorAlert(title: title, message: message, from: sourceVC, onPresented: nil, onDismissal: onDismissal)
+ }
+
+ static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?) {
+ showErrorAlert(title: title, message: message, from: sourceVC, onPresented: nil, onDismissal: nil)
+ }
+
+ static func showErrorAlert(error: WireGuardAppError, from sourceVC: AnyObject?, onPresented: (() -> Void)? = nil, onDismissal: (() -> Void)? = nil) {
+ let (title, message) = error.alertText
+ showErrorAlert(title: title, message: message, from: sourceVC, onPresented: onPresented, onDismissal: onDismissal)
+ }
+}
diff --git a/Sources/WireGuardApp/UI/LogViewHelper.swift b/Sources/WireGuardApp/UI/LogViewHelper.swift
new file mode 100644
index 0000000..1d3619b
--- /dev/null
+++ b/Sources/WireGuardApp/UI/LogViewHelper.swift
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+
+public class LogViewHelper {
+ var log: OpaquePointer
+ var cursor: UInt32 = UINT32_MAX
+ static let formatOptions: ISO8601DateFormatter.Options = [
+ .withYear, .withMonth, .withDay, .withTime,
+ .withDashSeparatorInDate, .withColonSeparatorInTime, .withSpaceBetweenDateAndTime,
+ .withFractionalSeconds
+ ]
+
+ struct LogEntry {
+ let timestamp: String
+ let message: String
+
+ func text() -> String {
+ return timestamp + " " + message
+ }
+ }
+
+ class LogEntries {
+ var entries: [LogEntry] = []
+ }
+
+ init?(logFilePath: String?) {
+ guard let logFilePath = logFilePath else { return nil }
+ guard let log = open_log(logFilePath) else { return nil }
+ self.log = log
+ }
+
+ deinit {
+ close_log(self.log)
+ }
+
+ func fetchLogEntriesSinceLastFetch(completion: @escaping ([LogViewHelper.LogEntry]) -> Void) {
+ var logEntries = LogEntries()
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+ guard let self = self else { return }
+ let newCursor = view_lines_from_cursor(self.log, self.cursor, &logEntries) { cStr, timestamp, ctx in
+ let message = cStr != nil ? String(cString: cStr!) : ""
+ let date = Date(timeIntervalSince1970: Double(timestamp) / 1000000000)
+ let dateString = ISO8601DateFormatter.string(from: date, timeZone: TimeZone.current, formatOptions: LogViewHelper.formatOptions)
+ if let logEntries = ctx?.bindMemory(to: LogEntries.self, capacity: 1) {
+ logEntries.pointee.entries.append(LogEntry(timestamp: dateString, message: message))
+ }
+ }
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+ self.cursor = newCursor
+ completion(logEntries.entries)
+ }
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/UI/PrivateDataConfirmation.swift b/Sources/WireGuardApp/UI/PrivateDataConfirmation.swift
new file mode 100644
index 0000000..c03e64a
--- /dev/null
+++ b/Sources/WireGuardApp/UI/PrivateDataConfirmation.swift
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+import LocalAuthentication
+#if os(macOS)
+import AppKit
+#endif
+
+class PrivateDataConfirmation {
+ static func confirmAccess(to reason: String, _ after: @escaping () -> Void) {
+ let context = LAContext()
+
+ var error: NSError?
+ if !context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
+ guard let error = error as? LAError else { return }
+ if error.code == .passcodeNotSet {
+ // We give no protection to folks who just don't set a passcode.
+ after()
+ }
+ return
+ }
+
+ context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
+ DispatchQueue.main.async {
+ #if os(macOS)
+ if !NSApp.isActive {
+ NSApp.activate(ignoringOtherApps: true)
+ }
+ #endif
+ if success {
+ after()
+ }
+ }
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/UI/TunnelImporter.swift b/Sources/WireGuardApp/UI/TunnelImporter.swift
new file mode 100644
index 0000000..3846382
--- /dev/null
+++ b/Sources/WireGuardApp/UI/TunnelImporter.swift
@@ -0,0 +1,95 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+import WireGuardKit
+
+class TunnelImporter {
+ static func importFromFile(urls: [URL], into tunnelsManager: TunnelsManager, sourceVC: AnyObject?, errorPresenterType: ErrorPresenterProtocol.Type, completionHandler: (() -> Void)? = nil) {
+ guard !urls.isEmpty else {
+ completionHandler?()
+ return
+ }
+ let dispatchGroup = DispatchGroup()
+ var configs = [TunnelConfiguration?]()
+ var lastFileImportErrorText: (title: String, message: String)?
+ for url in urls {
+ if url.pathExtension.lowercased() == "zip" {
+ dispatchGroup.enter()
+ ZipImporter.importConfigFiles(from: url) { result in
+ switch result {
+ case .failure(let error):
+ lastFileImportErrorText = error.alertText
+ case .success(let configsInZip):
+ configs.append(contentsOf: configsInZip)
+ }
+ dispatchGroup.leave()
+ }
+ } else { /* if it is not a zip, we assume it is a conf */
+ let fileName = url.lastPathComponent
+ let fileBaseName = url.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
+ dispatchGroup.enter()
+ DispatchQueue.global(qos: .userInitiated).async {
+ let fileContents: String
+ do {
+ fileContents = try String(contentsOf: url)
+ } catch let error {
+ DispatchQueue.main.async {
+ if let cocoaError = error as? CocoaError, cocoaError.isFileError {
+ lastFileImportErrorText = (title: tr("alertCantOpenInputConfFileTitle"), message: error.localizedDescription)
+ } else {
+ lastFileImportErrorText = (title: tr("alertCantOpenInputConfFileTitle"), message: tr(format: "alertCantOpenInputConfFileMessage (%@)", fileName))
+ }
+ configs.append(nil)
+ dispatchGroup.leave()
+ }
+ return
+ }
+ var parseError: Error?
+ var tunnelConfiguration: TunnelConfiguration?
+ do {
+ tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: fileContents, called: fileBaseName)
+ } catch let error {
+ parseError = error
+ }
+ DispatchQueue.main.async {
+ if parseError != nil {
+ if let parseError = parseError as? WireGuardAppError {
+ lastFileImportErrorText = parseError.alertText
+ } else {
+ lastFileImportErrorText = (title: tr("alertBadConfigImportTitle"), message: tr(format: "alertBadConfigImportMessage (%@)", fileName))
+ }
+ }
+ configs.append(tunnelConfiguration)
+ dispatchGroup.leave()
+ }
+ }
+ }
+ }
+ dispatchGroup.notify(queue: .main) {
+ tunnelsManager.addMultiple(tunnelConfigurations: configs.compactMap { $0 }) { numberSuccessful, lastAddError in
+ if !configs.isEmpty && numberSuccessful == configs.count {
+ completionHandler?()
+ return
+ }
+ let alertText: (title: String, message: String)?
+ if urls.count == 1 {
+ if urls.first!.pathExtension.lowercased() == "zip" && !configs.isEmpty {
+ alertText = (title: tr(format: "alertImportedFromZipTitle (%d)", numberSuccessful),
+ message: tr(format: "alertImportedFromZipMessage (%1$d of %2$d)", numberSuccessful, configs.count))
+ } else {
+ alertText = lastFileImportErrorText ?? lastAddError?.alertText
+ }
+ } else {
+ alertText = (title: tr(format: "alertImportedFromMultipleFilesTitle (%d)", numberSuccessful),
+ message: tr(format: "alertImportedFromMultipleFilesMessage (%1$d of %2$d)", numberSuccessful, configs.count))
+ }
+ if let alertText = alertText {
+ errorPresenterType.showErrorAlert(title: alertText.title, message: alertText.message, from: sourceVC, onPresented: completionHandler)
+ } else {
+ completionHandler?()
+ }
+ }
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/UI/TunnelViewModel.swift b/Sources/WireGuardApp/UI/TunnelViewModel.swift
new file mode 100644
index 0000000..3215b70
--- /dev/null
+++ b/Sources/WireGuardApp/UI/TunnelViewModel.swift
@@ -0,0 +1,696 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+import WireGuardKit
+
+class TunnelViewModel {
+
+ enum InterfaceField: CaseIterable {
+ case name
+ case privateKey
+ case publicKey
+ case generateKeyPair
+ case addresses
+ case listenPort
+ case mtu
+ case dns
+ case status
+ case toggleStatus
+
+ var localizedUIString: String {
+ switch self {
+ case .name: return tr("tunnelInterfaceName")
+ case .privateKey: return tr("tunnelInterfacePrivateKey")
+ case .publicKey: return tr("tunnelInterfacePublicKey")
+ case .generateKeyPair: return tr("tunnelInterfaceGenerateKeypair")
+ case .addresses: return tr("tunnelInterfaceAddresses")
+ case .listenPort: return tr("tunnelInterfaceListenPort")
+ case .mtu: return tr("tunnelInterfaceMTU")
+ case .dns: return tr("tunnelInterfaceDNS")
+ case .status: return tr("tunnelInterfaceStatus")
+ case .toggleStatus: return ""
+ }
+ }
+ }
+
+ static let interfaceFieldsWithControl: Set<InterfaceField> = [
+ .generateKeyPair
+ ]
+
+ enum PeerField: CaseIterable {
+ case publicKey
+ case preSharedKey
+ case endpoint
+ case persistentKeepAlive
+ case allowedIPs
+ case rxBytes
+ case txBytes
+ case lastHandshakeTime
+ case excludePrivateIPs
+ case deletePeer
+
+ var localizedUIString: String {
+ switch self {
+ case .publicKey: return tr("tunnelPeerPublicKey")
+ case .preSharedKey: return tr("tunnelPeerPreSharedKey")
+ case .endpoint: return tr("tunnelPeerEndpoint")
+ case .persistentKeepAlive: return tr("tunnelPeerPersistentKeepalive")
+ case .allowedIPs: return tr("tunnelPeerAllowedIPs")
+ case .rxBytes: return tr("tunnelPeerRxBytes")
+ case .txBytes: return tr("tunnelPeerTxBytes")
+ case .lastHandshakeTime: return tr("tunnelPeerLastHandshakeTime")
+ case .excludePrivateIPs: return tr("tunnelPeerExcludePrivateIPs")
+ case .deletePeer: return tr("deletePeerButtonTitle")
+ }
+ }
+ }
+
+ static let peerFieldsWithControl: Set<PeerField> = [
+ .excludePrivateIPs, .deletePeer
+ ]
+
+ static let keyLengthInBase64 = 44
+
+ struct Changes {
+ enum FieldChange: Equatable {
+ case added
+ case removed
+ case modified(newValue: String)
+ }
+
+ var interfaceChanges: [InterfaceField: FieldChange]
+ var peerChanges: [(peerIndex: Int, changes: [PeerField: FieldChange])]
+ var peersRemovedIndices: [Int]
+ var peersInsertedIndices: [Int]
+ }
+
+ class InterfaceData {
+ var scratchpad = [InterfaceField: String]()
+ var fieldsWithError = Set<InterfaceField>()
+ var validatedConfiguration: InterfaceConfiguration?
+ var validatedName: String?
+
+ subscript(field: InterfaceField) -> String {
+ get {
+ if scratchpad.isEmpty {
+ populateScratchpad()
+ }
+ return scratchpad[field] ?? ""
+ }
+ set(stringValue) {
+ if scratchpad.isEmpty {
+ populateScratchpad()
+ }
+ validatedConfiguration = nil
+ validatedName = nil
+ if stringValue.isEmpty {
+ scratchpad.removeValue(forKey: field)
+ } else {
+ scratchpad[field] = stringValue
+ }
+ if field == .privateKey {
+ if stringValue.count == TunnelViewModel.keyLengthInBase64,
+ let privateKey = PrivateKey(base64Key: stringValue) {
+ scratchpad[.publicKey] = privateKey.publicKey.base64Key
+ } else {
+ scratchpad.removeValue(forKey: .publicKey)
+ }
+ }
+ }
+ }
+
+ func populateScratchpad() {
+ guard let config = validatedConfiguration else { return }
+ guard let name = validatedName else { return }
+ scratchpad = TunnelViewModel.InterfaceData.createScratchPad(from: config, name: name)
+ }
+
+ private static func createScratchPad(from config: InterfaceConfiguration, name: String) -> [InterfaceField: String] {
+ var scratchpad = [InterfaceField: String]()
+ scratchpad[.name] = name
+ scratchpad[.privateKey] = config.privateKey.base64Key
+ scratchpad[.publicKey] = config.privateKey.publicKey.base64Key
+ if !config.addresses.isEmpty {
+ scratchpad[.addresses] = config.addresses.map { $0.stringRepresentation }.joined(separator: ", ")
+ }
+ if let listenPort = config.listenPort {
+ scratchpad[.listenPort] = String(listenPort)
+ }
+ if let mtu = config.mtu {
+ scratchpad[.mtu] = String(mtu)
+ }
+ if !config.dns.isEmpty {
+ scratchpad[.dns] = config.dns.map { $0.stringRepresentation }.joined(separator: ", ")
+ }
+ return scratchpad
+ }
+
+ func save() -> SaveResult<(String, InterfaceConfiguration)> {
+ if let config = validatedConfiguration, let name = validatedName {
+ return .saved((name, config))
+ }
+ fieldsWithError.removeAll()
+ guard let name = scratchpad[.name]?.trimmingCharacters(in: .whitespacesAndNewlines), (!name.isEmpty) else {
+ fieldsWithError.insert(.name)
+ return .error(tr("alertInvalidInterfaceMessageNameRequired"))
+ }
+ guard let privateKeyString = scratchpad[.privateKey] else {
+ fieldsWithError.insert(.privateKey)
+ return .error(tr("alertInvalidInterfaceMessagePrivateKeyRequired"))
+ }
+ guard let privateKey = PrivateKey(base64Key: privateKeyString) else {
+ fieldsWithError.insert(.privateKey)
+ return .error(tr("alertInvalidInterfaceMessagePrivateKeyInvalid"))
+ }
+ var config = InterfaceConfiguration(privateKey: privateKey)
+ var errorMessages = [String]()
+ if let addressesString = scratchpad[.addresses] {
+ var addresses = [IPAddressRange]()
+ for addressString in addressesString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
+ if let address = IPAddressRange(from: addressString) {
+ addresses.append(address)
+ } else {
+ fieldsWithError.insert(.addresses)
+ errorMessages.append(tr("alertInvalidInterfaceMessageAddressInvalid"))
+ }
+ }
+ config.addresses = addresses
+ }
+ if let listenPortString = scratchpad[.listenPort] {
+ if let listenPort = UInt16(listenPortString) {
+ config.listenPort = listenPort
+ } else {
+ fieldsWithError.insert(.listenPort)
+ errorMessages.append(tr("alertInvalidInterfaceMessageListenPortInvalid"))
+ }
+ }
+ if let mtuString = scratchpad[.mtu] {
+ if let mtu = UInt16(mtuString), mtu >= 576 {
+ config.mtu = mtu
+ } else {
+ fieldsWithError.insert(.mtu)
+ errorMessages.append(tr("alertInvalidInterfaceMessageMTUInvalid"))
+ }
+ }
+ if let dnsString = scratchpad[.dns] {
+ var dnsServers = [DNSServer]()
+ for dnsServerString in dnsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
+ if let dnsServer = DNSServer(from: dnsServerString) {
+ dnsServers.append(dnsServer)
+ } else {
+ fieldsWithError.insert(.dns)
+ errorMessages.append(tr("alertInvalidInterfaceMessageDNSInvalid"))
+ }
+ }
+ config.dns = dnsServers
+ }
+
+ guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
+
+ validatedConfiguration = config
+ validatedName = name
+ return .saved((name, config))
+ }
+
+ func filterFieldsWithValueOrControl(interfaceFields: [InterfaceField]) -> [InterfaceField] {
+ return interfaceFields.filter { field in
+ if TunnelViewModel.interfaceFieldsWithControl.contains(field) {
+ return true
+ }
+ return !self[field].isEmpty
+ }
+ }
+
+ func applyConfiguration(other: InterfaceConfiguration, otherName: String) -> [InterfaceField: Changes.FieldChange] {
+ if scratchpad.isEmpty {
+ populateScratchpad()
+ }
+ let otherScratchPad = InterfaceData.createScratchPad(from: other, name: otherName)
+ var changes = [InterfaceField: Changes.FieldChange]()
+ for field in InterfaceField.allCases {
+ switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
+ case ("", ""):
+ break
+ case ("", _):
+ changes[field] = .added
+ case (_, ""):
+ changes[field] = .removed
+ case (let this, let other):
+ if this != other {
+ changes[field] = .modified(newValue: other)
+ }
+ }
+ }
+ scratchpad = otherScratchPad
+ return changes
+ }
+ }
+
+ class PeerData {
+ var index: Int
+ var scratchpad = [PeerField: String]()
+ var fieldsWithError = Set<PeerField>()
+ var validatedConfiguration: PeerConfiguration?
+ var publicKey: PublicKey? {
+ if let validatedConfiguration = validatedConfiguration {
+ return validatedConfiguration.publicKey
+ }
+ if let scratchPadPublicKey = scratchpad[.publicKey] {
+ return PublicKey(base64Key: scratchPadPublicKey)
+ }
+ return nil
+ }
+
+ private(set) var shouldAllowExcludePrivateIPsControl = false
+ private(set) var shouldStronglyRecommendDNS = false
+ private(set) var excludePrivateIPsValue = false
+ fileprivate var numberOfPeers = 0
+
+ init(index: Int) {
+ self.index = index
+ }
+
+ subscript(field: PeerField) -> String {
+ get {
+ if scratchpad.isEmpty {
+ populateScratchpad()
+ }
+ return scratchpad[field] ?? ""
+ }
+ set(stringValue) {
+ if scratchpad.isEmpty {
+ populateScratchpad()
+ }
+ validatedConfiguration = nil
+ if stringValue.isEmpty {
+ scratchpad.removeValue(forKey: field)
+ } else {
+ scratchpad[field] = stringValue
+ }
+ if field == .allowedIPs {
+ updateExcludePrivateIPsFieldState()
+ }
+ }
+ }
+
+ func populateScratchpad() {
+ guard let config = validatedConfiguration else { return }
+ scratchpad = TunnelViewModel.PeerData.createScratchPad(from: config)
+ updateExcludePrivateIPsFieldState()
+ }
+
+ private static func createScratchPad(from config: PeerConfiguration) -> [PeerField: String] {
+ var scratchpad = [PeerField: String]()
+ scratchpad[.publicKey] = config.publicKey.base64Key
+ if let preSharedKey = config.preSharedKey?.base64Key {
+ scratchpad[.preSharedKey] = preSharedKey
+ }
+ if !config.allowedIPs.isEmpty {
+ scratchpad[.allowedIPs] = config.allowedIPs.map { $0.stringRepresentation }.joined(separator: ", ")
+ }
+ if let endpoint = config.endpoint {
+ scratchpad[.endpoint] = endpoint.stringRepresentation
+ }
+ if let persistentKeepAlive = config.persistentKeepAlive {
+ scratchpad[.persistentKeepAlive] = String(persistentKeepAlive)
+ }
+ if let rxBytes = config.rxBytes {
+ scratchpad[.rxBytes] = prettyBytes(rxBytes)
+ }
+ if let txBytes = config.txBytes {
+ scratchpad[.txBytes] = prettyBytes(txBytes)
+ }
+ if let lastHandshakeTime = config.lastHandshakeTime {
+ scratchpad[.lastHandshakeTime] = prettyTimeAgo(timestamp: lastHandshakeTime)
+ }
+ return scratchpad
+ }
+
+ func save() -> SaveResult<PeerConfiguration> {
+ if let validatedConfiguration = validatedConfiguration {
+ return .saved(validatedConfiguration)
+ }
+ fieldsWithError.removeAll()
+ guard let publicKeyString = scratchpad[.publicKey] else {
+ fieldsWithError.insert(.publicKey)
+ return .error(tr("alertInvalidPeerMessagePublicKeyRequired"))
+ }
+ guard let publicKey = PublicKey(base64Key: publicKeyString) else {
+ fieldsWithError.insert(.publicKey)
+ return .error(tr("alertInvalidPeerMessagePublicKeyInvalid"))
+ }
+ var config = PeerConfiguration(publicKey: publicKey)
+ var errorMessages = [String]()
+ if let preSharedKeyString = scratchpad[.preSharedKey] {
+ if let preSharedKey = PreSharedKey(base64Key: preSharedKeyString) {
+ config.preSharedKey = preSharedKey
+ } else {
+ fieldsWithError.insert(.preSharedKey)
+ errorMessages.append(tr("alertInvalidPeerMessagePreSharedKeyInvalid"))
+ }
+ }
+ if let allowedIPsString = scratchpad[.allowedIPs] {
+ var allowedIPs = [IPAddressRange]()
+ for allowedIPString in allowedIPsString.splitToArray(trimmingCharacters: .whitespacesAndNewlines) {
+ if let allowedIP = IPAddressRange(from: allowedIPString) {
+ allowedIPs.append(allowedIP)
+ } else {
+ fieldsWithError.insert(.allowedIPs)
+ errorMessages.append(tr("alertInvalidPeerMessageAllowedIPsInvalid"))
+ }
+ }
+ config.allowedIPs = allowedIPs
+ }
+ if let endpointString = scratchpad[.endpoint] {
+ if let endpoint = Endpoint(from: endpointString) {
+ config.endpoint = endpoint
+ } else {
+ fieldsWithError.insert(.endpoint)
+ errorMessages.append(tr("alertInvalidPeerMessageEndpointInvalid"))
+ }
+ }
+ if let persistentKeepAliveString = scratchpad[.persistentKeepAlive] {
+ if let persistentKeepAlive = UInt16(persistentKeepAliveString) {
+ config.persistentKeepAlive = persistentKeepAlive
+ } else {
+ fieldsWithError.insert(.persistentKeepAlive)
+ errorMessages.append(tr("alertInvalidPeerMessagePersistentKeepaliveInvalid"))
+ }
+ }
+
+ guard errorMessages.isEmpty else { return .error(errorMessages.first!) }
+
+ validatedConfiguration = config
+ return .saved(config)
+ }
+
+ func filterFieldsWithValueOrControl(peerFields: [PeerField]) -> [PeerField] {
+ return peerFields.filter { field in
+ if TunnelViewModel.peerFieldsWithControl.contains(field) {
+ return true
+ }
+ return (!self[field].isEmpty)
+ }
+ }
+
+ static let ipv4DefaultRouteString = "0.0.0.0/0"
+ static let ipv4DefaultRouteModRFC1918String = [ // Set of all non-private IPv4 IPs
+ "0.0.0.0/5", "8.0.0.0/7", "11.0.0.0/8", "12.0.0.0/6", "16.0.0.0/4", "32.0.0.0/3",
+ "64.0.0.0/2", "128.0.0.0/3", "160.0.0.0/5", "168.0.0.0/6", "172.0.0.0/12",
+ "172.32.0.0/11", "172.64.0.0/10", "172.128.0.0/9", "173.0.0.0/8", "174.0.0.0/7",
+ "176.0.0.0/4", "192.0.0.0/9", "192.128.0.0/11", "192.160.0.0/13", "192.169.0.0/16",
+ "192.170.0.0/15", "192.172.0.0/14", "192.176.0.0/12", "192.192.0.0/10",
+ "193.0.0.0/8", "194.0.0.0/7", "196.0.0.0/6", "200.0.0.0/5", "208.0.0.0/4"
+ ]
+
+ static func excludePrivateIPsFieldStates(isSinglePeer: Bool, allowedIPs: Set<String>) -> (shouldAllowExcludePrivateIPsControl: Bool, excludePrivateIPsValue: Bool) {
+ guard isSinglePeer else {
+ return (shouldAllowExcludePrivateIPsControl: false, excludePrivateIPsValue: false)
+ }
+ let allowedIPStrings = Set<String>(allowedIPs)
+ if allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) {
+ return (shouldAllowExcludePrivateIPsControl: true, excludePrivateIPsValue: false)
+ } else if allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String) {
+ return (shouldAllowExcludePrivateIPsControl: true, excludePrivateIPsValue: true)
+ } else {
+ return (shouldAllowExcludePrivateIPsControl: false, excludePrivateIPsValue: false)
+ }
+ }
+
+ func updateExcludePrivateIPsFieldState() {
+ if scratchpad.isEmpty {
+ populateScratchpad()
+ }
+ let allowedIPStrings = Set<String>(scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines))
+ (shouldAllowExcludePrivateIPsControl, excludePrivateIPsValue) = TunnelViewModel.PeerData.excludePrivateIPsFieldStates(isSinglePeer: numberOfPeers == 1, allowedIPs: allowedIPStrings)
+ shouldStronglyRecommendDNS = allowedIPStrings.contains(TunnelViewModel.PeerData.ipv4DefaultRouteString) || allowedIPStrings.isSuperset(of: TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String)
+ }
+
+ static func normalizedIPAddressRangeStrings(_ list: [String]) -> [String] {
+ return list.compactMap { IPAddressRange(from: $0) }.map { $0.stringRepresentation }
+ }
+
+ static func modifiedAllowedIPs(currentAllowedIPs: [String], excludePrivateIPs: Bool, dnsServers: [String], oldDNSServers: [String]?) -> [String] {
+ let normalizedDNSServers = normalizedIPAddressRangeStrings(dnsServers)
+ let normalizedOldDNSServers = oldDNSServers == nil ? normalizedDNSServers : normalizedIPAddressRangeStrings(oldDNSServers!)
+ let ipv6Addresses = normalizedIPAddressRangeStrings(currentAllowedIPs.filter { $0.contains(":") })
+ if excludePrivateIPs {
+ return ipv6Addresses + TunnelViewModel.PeerData.ipv4DefaultRouteModRFC1918String + normalizedDNSServers
+ } else {
+ return ipv6Addresses.filter { !normalizedOldDNSServers.contains($0) } + [TunnelViewModel.PeerData.ipv4DefaultRouteString]
+ }
+ }
+
+ func excludePrivateIPsValueChanged(isOn: Bool, dnsServers: String, oldDNSServers: String? = nil) {
+ let allowedIPStrings = scratchpad[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
+ let dnsServerStrings = dnsServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines)
+ let oldDNSServerStrings = oldDNSServers?.splitToArray(trimmingCharacters: .whitespacesAndNewlines)
+ let modifiedAllowedIPStrings = TunnelViewModel.PeerData.modifiedAllowedIPs(currentAllowedIPs: allowedIPStrings, excludePrivateIPs: isOn, dnsServers: dnsServerStrings, oldDNSServers: oldDNSServerStrings)
+ scratchpad[.allowedIPs] = modifiedAllowedIPStrings.joined(separator: ", ")
+ validatedConfiguration = nil
+ excludePrivateIPsValue = isOn
+ }
+
+ func applyConfiguration(other: PeerConfiguration) -> [PeerField: Changes.FieldChange] {
+ if scratchpad.isEmpty {
+ populateScratchpad()
+ }
+ let otherScratchPad = PeerData.createScratchPad(from: other)
+ var changes = [PeerField: Changes.FieldChange]()
+ for field in PeerField.allCases {
+ switch (scratchpad[field] ?? "", otherScratchPad[field] ?? "") {
+ case ("", ""):
+ break
+ case ("", _):
+ changes[field] = .added
+ case (_, ""):
+ changes[field] = .removed
+ case (let this, let other):
+ if this != other {
+ changes[field] = .modified(newValue: other)
+ }
+ }
+ }
+ scratchpad = otherScratchPad
+ return changes
+ }
+ }
+
+ enum SaveResult<Configuration> {
+ case saved(Configuration)
+ case error(String)
+ }
+
+ private(set) var interfaceData: InterfaceData
+ private(set) var peersData: [PeerData]
+
+ init(tunnelConfiguration: TunnelConfiguration?) {
+ let interfaceData = InterfaceData()
+ var peersData = [PeerData]()
+ if let tunnelConfiguration = tunnelConfiguration {
+ interfaceData.validatedConfiguration = tunnelConfiguration.interface
+ interfaceData.validatedName = tunnelConfiguration.name
+ for (index, peerConfiguration) in tunnelConfiguration.peers.enumerated() {
+ let peerData = PeerData(index: index)
+ peerData.validatedConfiguration = peerConfiguration
+ peersData.append(peerData)
+ }
+ }
+ let numberOfPeers = peersData.count
+ for peerData in peersData {
+ peerData.numberOfPeers = numberOfPeers
+ peerData.updateExcludePrivateIPsFieldState()
+ }
+ self.interfaceData = interfaceData
+ self.peersData = peersData
+ }
+
+ func appendEmptyPeer() {
+ let peer = PeerData(index: peersData.count)
+ peersData.append(peer)
+ for peer in peersData {
+ peer.numberOfPeers = peersData.count
+ peer.updateExcludePrivateIPsFieldState()
+ }
+ }
+
+ func deletePeer(peer: PeerData) {
+ let removedPeer = peersData.remove(at: peer.index)
+ assert(removedPeer.index == peer.index)
+ for peer in peersData[peer.index ..< peersData.count] {
+ assert(peer.index > 0)
+ peer.index -= 1
+ }
+ for peer in peersData {
+ peer.numberOfPeers = peersData.count
+ peer.updateExcludePrivateIPsFieldState()
+ }
+ }
+
+ func updateDNSServersInAllowedIPsIfRequired(oldDNSServers: String, newDNSServers: String) -> Bool {
+ guard peersData.count == 1, let firstPeer = peersData.first else { return false }
+ guard firstPeer.shouldAllowExcludePrivateIPsControl && firstPeer.excludePrivateIPsValue else { return false }
+ let allowedIPStrings = firstPeer[.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
+ let oldDNSServerStrings = TunnelViewModel.PeerData.normalizedIPAddressRangeStrings(oldDNSServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines))
+ let newDNSServerStrings = TunnelViewModel.PeerData.normalizedIPAddressRangeStrings(newDNSServers.splitToArray(trimmingCharacters: .whitespacesAndNewlines))
+ let updatedAllowedIPStrings = allowedIPStrings.filter { !oldDNSServerStrings.contains($0) } + newDNSServerStrings
+ firstPeer[.allowedIPs] = updatedAllowedIPStrings.joined(separator: ", ")
+ return true
+ }
+
+ func save() -> SaveResult<TunnelConfiguration> {
+ let interfaceSaveResult = interfaceData.save()
+ let peerSaveResults = peersData.map { $0.save() } // Save all, to help mark erroring fields in red
+ switch interfaceSaveResult {
+ case .error(let errorMessage):
+ return .error(errorMessage)
+ case .saved(let interfaceConfiguration):
+ var peerConfigurations = [PeerConfiguration]()
+ peerConfigurations.reserveCapacity(peerSaveResults.count)
+ for peerSaveResult in peerSaveResults {
+ switch peerSaveResult {
+ case .error(let errorMessage):
+ return .error(errorMessage)
+ case .saved(let peerConfiguration):
+ peerConfigurations.append(peerConfiguration)
+ }
+ }
+
+ let peerPublicKeysArray = peerConfigurations.map { $0.publicKey }
+ let peerPublicKeysSet = Set<PublicKey>(peerPublicKeysArray)
+ if peerPublicKeysArray.count != peerPublicKeysSet.count {
+ return .error(tr("alertInvalidPeerMessagePublicKeyDuplicated"))
+ }
+
+ let tunnelConfiguration = TunnelConfiguration(name: interfaceConfiguration.0, interface: interfaceConfiguration.1, peers: peerConfigurations)
+ return .saved(tunnelConfiguration)
+ }
+ }
+
+ func asWgQuickConfig() -> String? {
+ let saveResult = save()
+ if case .saved(let tunnelConfiguration) = saveResult {
+ return tunnelConfiguration.asWgQuickConfig()
+ }
+ return nil
+ }
+
+ @discardableResult
+ func applyConfiguration(other: TunnelConfiguration) -> Changes {
+ // Replaces current data with data from other TunnelConfiguration, ignoring any changes in peer ordering.
+
+ let interfaceChanges = interfaceData.applyConfiguration(other: other.interface, otherName: other.name ?? "")
+
+ var peerChanges = [(peerIndex: Int, changes: [PeerField: Changes.FieldChange])]()
+ for otherPeer in other.peers {
+ if let peersDataIndex = peersData.firstIndex(where: { $0.publicKey == otherPeer.publicKey }) {
+ let peerData = peersData[peersDataIndex]
+ let changes = peerData.applyConfiguration(other: otherPeer)
+ if !changes.isEmpty {
+ peerChanges.append((peerIndex: peersDataIndex, changes: changes))
+ }
+ }
+ }
+
+ var removedPeerIndices = [Int]()
+ for (index, peerData) in peersData.enumerated().reversed() {
+ if let peerPublicKey = peerData.publicKey, !other.peers.contains(where: { $0.publicKey == peerPublicKey}) {
+ removedPeerIndices.append(index)
+ peersData.remove(at: index)
+ }
+ }
+
+ var addedPeerIndices = [Int]()
+ for otherPeer in other.peers {
+ if !peersData.contains(where: { $0.publicKey == otherPeer.publicKey }) {
+ addedPeerIndices.append(peersData.count)
+ let peerData = PeerData(index: peersData.count)
+ peerData.validatedConfiguration = otherPeer
+ peersData.append(peerData)
+ }
+ }
+
+ for (index, peer) in peersData.enumerated() {
+ peer.index = index
+ peer.numberOfPeers = peersData.count
+ peer.updateExcludePrivateIPsFieldState()
+ }
+
+ return Changes(interfaceChanges: interfaceChanges, peerChanges: peerChanges, peersRemovedIndices: removedPeerIndices, peersInsertedIndices: addedPeerIndices)
+ }
+}
+
+private func prettyBytes(_ bytes: UInt64) -> String {
+ switch bytes {
+ case 0..<1024:
+ return "\(bytes) B"
+ case 1024 ..< (1024 * 1024):
+ return String(format: "%.2f", Double(bytes) / 1024) + " KiB"
+ case 1024 ..< (1024 * 1024 * 1024):
+ return String(format: "%.2f", Double(bytes) / (1024 * 1024)) + " MiB"
+ case 1024 ..< (1024 * 1024 * 1024 * 1024):
+ return String(format: "%.2f", Double(bytes) / (1024 * 1024 * 1024)) + " GiB"
+ default:
+ return String(format: "%.2f", Double(bytes) / (1024 * 1024 * 1024 * 1024)) + " TiB"
+ }
+}
+
+private func prettyTimeAgo(timestamp: Date) -> String {
+ let now = Date()
+ let timeInterval = Int64(now.timeIntervalSince(timestamp))
+ switch timeInterval {
+ case ..<0: return tr("tunnelHandshakeTimestampSystemClockBackward")
+ case 0: return tr("tunnelHandshakeTimestampNow")
+ default:
+ return tr(format: "tunnelHandshakeTimestampAgo (%@)", prettyTime(secondsLeft: timeInterval))
+ }
+}
+
+private func prettyTime(secondsLeft: Int64) -> String {
+ var left = secondsLeft
+ var timeStrings = [String]()
+ let years = left / (365 * 24 * 60 * 60)
+ left = left % (365 * 24 * 60 * 60)
+ let days = left / (24 * 60 * 60)
+ left = left % (24 * 60 * 60)
+ let hours = left / (60 * 60)
+ left = left % (60 * 60)
+ let minutes = left / 60
+ let seconds = left % 60
+
+ #if os(iOS)
+ if years > 0 {
+ return years == 1 ? tr(format: "tunnelHandshakeTimestampYear (%d)", years) : tr(format: "tunnelHandshakeTimestampYears (%d)", years)
+ }
+ if days > 0 {
+ return days == 1 ? tr(format: "tunnelHandshakeTimestampDay (%d)", days) : tr(format: "tunnelHandshakeTimestampDays (%d)", days)
+ }
+ if hours > 0 {
+ let hhmmss = String(format: "%02d:%02d:%02d", hours, minutes, seconds)
+ return tr(format: "tunnelHandshakeTimestampHours hh:mm:ss (%@)", hhmmss)
+ }
+ if minutes > 0 {
+ let mmss = String(format: "%02d:%02d", minutes, seconds)
+ return tr(format: "tunnelHandshakeTimestampMinutes mm:ss (%@)", mmss)
+ }
+ return seconds == 1 ? tr(format: "tunnelHandshakeTimestampSecond (%d)", seconds) : tr(format: "tunnelHandshakeTimestampSeconds (%d)", seconds)
+ #elseif os(macOS)
+ if years > 0 {
+ timeStrings.append(years == 1 ? tr(format: "tunnelHandshakeTimestampYear (%d)", years) : tr(format: "tunnelHandshakeTimestampYears (%d)", years))
+ }
+ if days > 0 {
+ timeStrings.append(days == 1 ? tr(format: "tunnelHandshakeTimestampDay (%d)", days) : tr(format: "tunnelHandshakeTimestampDays (%d)", days))
+ }
+ if hours > 0 {
+ timeStrings.append(hours == 1 ? tr(format: "tunnelHandshakeTimestampHour (%d)", hours) : tr(format: "tunnelHandshakeTimestampHours (%d)", hours))
+ }
+ if minutes > 0 {
+ timeStrings.append(minutes == 1 ? tr(format: "tunnelHandshakeTimestampMinute (%d)", minutes) : tr(format: "tunnelHandshakeTimestampMinutes (%d)", minutes))
+ }
+ if seconds > 0 {
+ timeStrings.append(seconds == 1 ? tr(format: "tunnelHandshakeTimestampSecond (%d)", seconds) : tr(format: "tunnelHandshakeTimestampSeconds (%d)", seconds))
+ }
+ return timeStrings.joined(separator: ", ")
+ #endif
+}
diff --git a/Sources/WireGuardApp/UI/iOS/AppDelegate.swift b/Sources/WireGuardApp/UI/iOS/AppDelegate.swift
new file mode 100644
index 0000000..418557e
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/AppDelegate.swift
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+import os.log
+
+@UIApplicationMain
+class AppDelegate: UIResponder, UIApplicationDelegate {
+
+ var window: UIWindow?
+ var mainVC: MainViewController?
+ var isLaunchedForSpecificAction = false
+
+ func application(_ application: UIApplication, willFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
+ Logger.configureGlobal(tagged: "APP", withFilePath: FileManager.logFileURL?.path)
+
+ if let launchOptions = launchOptions {
+ if launchOptions[.url] != nil || launchOptions[.shortcutItem] != nil {
+ isLaunchedForSpecificAction = true
+ }
+ }
+
+ let window = UIWindow(frame: UIScreen.main.bounds)
+ if #available(iOS 13.0, *) {
+ window.backgroundColor = .systemBackground
+ } else {
+ window.backgroundColor = .white
+ }
+ self.window = window
+
+ let mainVC = MainViewController()
+ window.rootViewController = mainVC
+ window.makeKeyAndVisible()
+
+ self.mainVC = mainVC
+
+ return true
+ }
+
+ func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
+ mainVC?.importFromDisposableFile(url: url)
+ return true
+ }
+
+ func applicationDidBecomeActive(_ application: UIApplication) {
+ mainVC?.refreshTunnelConnectionStatuses()
+ }
+
+ func applicationWillResignActive(_ application: UIApplication) {
+ guard let allTunnelNames = mainVC?.allTunnelNames() else { return }
+ application.shortcutItems = QuickActionItem.createItems(allTunnelNames: allTunnelNames)
+ }
+
+ func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
+ guard shortcutItem.type == QuickActionItem.type else {
+ completionHandler(false)
+ return
+ }
+ let tunnelName = shortcutItem.localizedTitle
+ mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false, shouldToggleStatus: true)
+ completionHandler(true)
+ }
+}
+
+extension AppDelegate {
+ func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
+ return true
+ }
+
+ func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
+ return !self.isLaunchedForSpecificAction
+ }
+
+ func application(_ application: UIApplication, viewControllerWithRestorationIdentifierPath identifierComponents: [String], coder: NSCoder) -> UIViewController? {
+ guard let vcIdentifier = identifierComponents.last else { return nil }
+ if vcIdentifier.hasPrefix("TunnelDetailVC:") {
+ let tunnelName = String(vcIdentifier.suffix(vcIdentifier.count - "TunnelDetailVC:".count))
+ if let tunnelsManager = mainVC?.tunnelsManager {
+ if let tunnel = tunnelsManager.tunnel(named: tunnelName) {
+ return TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
+ }
+ } else {
+ // Show it when tunnelsManager is available
+ mainVC?.showTunnelDetailForTunnel(named: tunnelName, animated: false, shouldToggleStatus: false)
+ }
+ }
+ return nil
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..9f4dceb
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,116 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "wireguard_logo_20pt@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "wireguard_logo_20pt@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "wireguard_logo_29pt@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "wireguard_logo_29pt@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "wireguard_logo_40pt@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "wireguard_logo_40pt@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "wireguard_logo_60pt@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "wireguard_logo_60pt@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "wireguard_logo_20pt@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "wireguard_logo_20pt@2x-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "wireguard_logo_29pt@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "wireguard_logo_29pt@2x-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "wireguard_logo_40pt@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "wireguard_logo_40pt@2x-1.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "wireguard_logo_76pt@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "wireguard_logo_76pt@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "wireguard_logo_83.5pt@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "wireguard_logo.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo.png
new file mode 100644
index 0000000..ec74f4d
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@1x.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@1x.png
new file mode 100644
index 0000000..e0dc54d
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@1x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@2x-1.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@2x-1.png
new file mode 100644
index 0000000..429297a
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@2x-1.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@2x.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@2x.png
new file mode 100644
index 0000000..429297a
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@2x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@3x.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@3x.png
new file mode 100644
index 0000000..eb79183
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_20pt@3x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@1x.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@1x.png
new file mode 100644
index 0000000..a8fd5c2
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@1x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@2x-1.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@2x-1.png
new file mode 100644
index 0000000..3645f0e
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@2x-1.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@2x.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@2x.png
new file mode 100644
index 0000000..3645f0e
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@2x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@3x.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@3x.png
new file mode 100644
index 0000000..386bb88
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_29pt@3x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@1x.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@1x.png
new file mode 100644
index 0000000..429297a
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@1x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@2x-1.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@2x-1.png
new file mode 100644
index 0000000..49bfff8
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@2x-1.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@2x.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@2x.png
new file mode 100644
index 0000000..49bfff8
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@2x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@3x.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@3x.png
new file mode 100644
index 0000000..8aa7b6c
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_40pt@3x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_60pt@2x.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_60pt@2x.png
new file mode 100644
index 0000000..8aa7b6c
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_60pt@2x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_60pt@3x.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_60pt@3x.png
new file mode 100644
index 0000000..f6c3318
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_60pt@3x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_76pt@1x.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_76pt@1x.png
new file mode 100644
index 0000000..b0e2c3c
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_76pt@1x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_76pt@2x.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_76pt@2x.png
new file mode 100644
index 0000000..0c708bc
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_76pt@2x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_83.5pt@2x.png b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_83.5pt@2x.png
new file mode 100644
index 0000000..b7338c4
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/AppIcon.appiconset/wireguard_logo_83.5pt@2x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/Contents.json b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..da4a164
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/wireguard.imageset/Contents.json b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/wireguard.imageset/Contents.json
new file mode 100644
index 0000000..6c935c1
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/wireguard.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "wireguard.pdf"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "preserves-vector-representation" : true
+ }
+} \ No newline at end of file
diff --git a/Sources/WireGuardApp/UI/iOS/Assets.xcassets/wireguard.imageset/wireguard.pdf b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/wireguard.imageset/wireguard.pdf
new file mode 100644
index 0000000..69469c5
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Assets.xcassets/wireguard.imageset/wireguard.pdf
Binary files differ
diff --git a/Sources/WireGuardApp/UI/iOS/Base.lproj/LaunchScreen.storyboard b/Sources/WireGuardApp/UI/iOS/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..a88aa5b
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14460.31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="dyO-pm-zxZ">
+ <device id="ipad9_7" orientation="landscape">
+ <adaptation id="fullscreen"/>
+ </device>
+ <dependencies>
+ <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14460.20"/>
+ <capability name="Safe area layout guides" minToolsVersion="9.0"/>
+ <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+ </dependencies>
+ <scenes>
+ <!--View Controller-->
+ <scene sceneID="wbj-wA-LRS">
+ <objects>
+ <viewController id="xPs-rU-RaC" sceneMemberID="viewController">
+ <view key="view" contentMode="scaleToFill" id="VmF-QJ-FIU">
+ <rect key="frame" x="0.0" y="0.0" width="703.5" height="768"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ <viewLayoutGuide key="safeArea" id="gU2-7a-hJw"/>
+ </view>
+ </viewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="XJD-RE-ATd" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ </scene>
+ <!--Table View Controller-->
+ <scene sceneID="pXL-Um-fWR">
+ <objects>
+ <tableViewController clearsSelectionOnViewWillAppear="NO" id="9s0-Gz-xEQ" sceneMemberID="viewController">
+ <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="PWV-jc-Hj5">
+ <rect key="frame" x="0.0" y="0.0" width="320" height="768"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+ <prototypes>
+ <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="LaunchScreenCellReuseId" id="bv7-0X-YhD">
+ <rect key="frame" x="0.0" y="28" width="320" height="44"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="bv7-0X-YhD" id="Uag-Pa-rmu">
+ <rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
+ <autoresizingMask key="autoresizingMask"/>
+ </tableViewCellContentView>
+ </tableViewCell>
+ </prototypes>
+ <connections>
+ <outlet property="dataSource" destination="9s0-Gz-xEQ" id="djY-Us-7mp"/>
+ <outlet property="delegate" destination="9s0-Gz-xEQ" id="Frz-TZ-bD1"/>
+ </connections>
+ </tableView>
+ <navigationItem key="navigationItem" id="0ZE-eq-OPY"/>
+ </tableViewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="YJ1-t3-sLe" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ </scene>
+ <!--Navigation Controller-->
+ <scene sceneID="bPm-bN-3Gh">
+ <objects>
+ <navigationController id="Fi9-6j-pBd" sceneMemberID="viewController">
+ <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="h1o-PJ-Yrv">
+ <rect key="frame" x="0.0" y="20" width="320" height="50"/>
+ <autoresizingMask key="autoresizingMask"/>
+ </navigationBar>
+ <connections>
+ <segue destination="9s0-Gz-xEQ" kind="relationship" relationship="rootViewController" id="Gdt-8A-Sqj"/>
+ </connections>
+ </navigationController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="fqW-5d-sYk" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ </scene>
+ <!--Split View Controller-->
+ <scene sceneID="FTm-2q-Sdy">
+ <objects>
+ <splitViewController id="dyO-pm-zxZ" sceneMemberID="viewController">
+ <connections>
+ <segue destination="Fi9-6j-pBd" kind="relationship" relationship="masterViewController" id="mFt-pZ-wHb"/>
+ <segue destination="xPs-rU-RaC" kind="relationship" relationship="detailViewController" id="q2k-Zf-dXJ"/>
+ </connections>
+ </splitViewController>
+ <placeholder placeholderIdentifier="IBFirstResponder" id="XEu-8J-cff" userLabel="First Responder" sceneMemberID="firstResponder"/>
+ </objects>
+ </scene>
+ </scenes>
+</document>
diff --git a/Sources/WireGuardApp/UI/iOS/ConfirmationAlertPresenter.swift b/Sources/WireGuardApp/UI/iOS/ConfirmationAlertPresenter.swift
new file mode 100644
index 0000000..8064fff
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/ConfirmationAlertPresenter.swift
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class ConfirmationAlertPresenter {
+ static func showConfirmationAlert(message: String, buttonTitle: String, from sourceObject: AnyObject, presentingVC: UIViewController, onConfirmed: @escaping (() -> Void)) {
+ let destroyAction = UIAlertAction(title: buttonTitle, style: .destructive) { _ in
+ onConfirmed()
+ }
+ let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
+ let alert = UIAlertController(title: "", message: message, preferredStyle: .actionSheet)
+ alert.addAction(destroyAction)
+ alert.addAction(cancelAction)
+
+ if let sourceView = sourceObject as? UIView {
+ alert.popoverPresentationController?.sourceView = sourceView
+ alert.popoverPresentationController?.sourceRect = sourceView.bounds
+ } else if let sourceBarButtonItem = sourceObject as? UIBarButtonItem {
+ alert.popoverPresentationController?.barButtonItem = sourceBarButtonItem
+ }
+
+ presentingVC.present(alert, animated: true, completion: nil)
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/ErrorPresenter.swift b/Sources/WireGuardApp/UI/iOS/ErrorPresenter.swift
new file mode 100644
index 0000000..7d134de
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/ErrorPresenter.swift
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+import os.log
+
+class ErrorPresenter: ErrorPresenterProtocol {
+ static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?, onDismissal: (() -> Void)?) {
+ guard let sourceVC = sourceVC as? UIViewController else { return }
+
+ let okAction = UIAlertAction(title: "OK", style: .default) { _ in
+ onDismissal?()
+ }
+ let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
+ alert.addAction(okAction)
+
+ sourceVC.present(alert, animated: true, completion: onPresented)
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/Info.plist b/Sources/WireGuardApp/UI/iOS/Info.plist
new file mode 100644
index 0000000..7d91077
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/Info.plist
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="UTF-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>ITSAppUsesNonExemptEncryption</key>
+ <false/>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleDocumentTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleTypeExtensions</key>
+ <array>
+ <string>conf</string>
+ </array>
+ <key>CFBundleTypeIconFiles</key>
+ <array>
+ <string>wireguard_doc_logo_22x29.png</string>
+ <string>wireguard_doc_logo_44x58.png</string>
+ <string>wireguard_doc_logo_64x64.png</string>
+ <string>wireguard_doc_logo_320x320.png</string>
+ </array>
+ <key>CFBundleTypeName</key>
+ <string>WireGuard wg-quick configuration file</string>
+ <key>CFBundleTypeRole</key>
+ <string>Viewer</string>
+ <key>LSHandlerRank</key>
+ <string>Default</string>
+ <key>LSItemContentTypes</key>
+ <array>
+ <string>com.wireguard.config.quick</string>
+ </array>
+ </dict>
+ <dict>
+ <key>CFBundleTypeName</key>
+ <string>Zip file</string>
+ <key>CFBundleTypeRole</key>
+ <string>Viewer</string>
+ <key>LSHandlerRank</key>
+ <string>Alternate</string>
+ <key>LSItemContentTypes</key>
+ <array>
+ <string>com.pkware.zip-archive</string>
+ </array>
+ </dict>
+ <dict>
+ <key>CFBundleTypeIconFiles</key>
+ <array/>
+ <key>CFBundleTypeName</key>
+ <string>Text file</string>
+ <key>CFBundleTypeRole</key>
+ <string>Viewer</string>
+ <key>LSHandlerRank</key>
+ <string>Alternate</string>
+ <key>LSItemContentTypes</key>
+ <array>
+ <string>public.text</string>
+ </array>
+ </dict>
+ </array>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIdentifier</key>
+ <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>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>$(VERSION_NAME)</string>
+ <key>CFBundleVersion</key>
+ <string>$(VERSION_ID)</string>
+ <key>LSRequiresIPhoneOS</key>
+ <true/>
+ <key>LSSupportsOpeningDocumentsInPlace</key>
+ <false/>
+ <key>NSCameraUsageDescription</key>
+ <string>Localized</string>
+ <key>UILaunchStoryboardName</key>
+ <string>LaunchScreen</string>
+ <key>UIRequiredDeviceCapabilities</key>
+ <array>
+ </array>
+ <key>UISupportedInterfaceOrientations</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UISupportedInterfaceOrientations~ipad</key>
+ <array>
+ <string>UIInterfaceOrientationPortrait</string>
+ <string>UIInterfaceOrientationPortraitUpsideDown</string>
+ <string>UIInterfaceOrientationLandscapeLeft</string>
+ <string>UIInterfaceOrientationLandscapeRight</string>
+ </array>
+ <key>UTExportedTypeDeclarations</key>
+ <array>
+ <dict>
+ <key>UTTypeConformsTo</key>
+ <array>
+ <string>public.text</string>
+ </array>
+ <key>UTTypeDescription</key>
+ <string>WireGuard wg-quick configuration file</string>
+ <key>UTTypeIconFiles</key>
+ <array>
+ <string>wireguard_doc_logo_22x29.png</string>
+ <string>wireguard_doc_logo_44x58.png</string>
+ <string>wireguard_doc_logo_64x64.png</string>
+ <string>wireguard_doc_logo_320x320.png</string>
+ </array>
+ <key>UTTypeIdentifier</key>
+ <string>com.wireguard.config.quick</string>
+ <key>UTTypeTagSpecification</key>
+ <dict>
+ <key>public.filename-extension</key>
+ <string>conf</string>
+ </dict>
+ </dict>
+ </array>
+ <key>NSFaceIDUsageDescription</key>
+ <string>Localized</string>
+ <key>com.wireguard.ios.app_group_id</key>
+ <string>group.$(APP_ID_IOS)</string>
+</dict>
+</plist>
diff --git a/Sources/WireGuardApp/UI/iOS/QuickActionItem.swift b/Sources/WireGuardApp/UI/iOS/QuickActionItem.swift
new file mode 100644
index 0000000..4367fa9
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/QuickActionItem.swift
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class QuickActionItem: UIApplicationShortcutItem {
+ static let type = "WireGuardTunnelActivateAndShow"
+
+ init(tunnelName: String) {
+ super.init(type: QuickActionItem.type, localizedTitle: tunnelName, localizedSubtitle: nil, icon: nil, userInfo: nil)
+ }
+
+ static func createItems(allTunnelNames: [String]) -> [QuickActionItem] {
+ let numberOfItems = 10
+ // Currently, only 4 items shown by iOS, but that can increase in the future.
+ // iOS will discard additional items we give it.
+ var tunnelNames = RecentTunnelsTracker.recentlyActivatedTunnelNames(limit: numberOfItems)
+ let numberOfSlotsRemaining = numberOfItems - tunnelNames.count
+ if numberOfSlotsRemaining > 0 {
+ let moreTunnels = allTunnelNames.filter { !tunnelNames.contains($0) }.prefix(numberOfSlotsRemaining)
+ tunnelNames.append(contentsOf: moreTunnels)
+ }
+ return tunnelNames.map { QuickActionItem(tunnelName: $0) }
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/RecentTunnelsTracker.swift b/Sources/WireGuardApp/UI/iOS/RecentTunnelsTracker.swift
new file mode 100644
index 0000000..787a624
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/RecentTunnelsTracker.swift
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+
+class RecentTunnelsTracker {
+
+ private static let keyRecentlyActivatedTunnelNames = "recentlyActivatedTunnelNames"
+ private static let maxNumberOfTunnels = 10
+
+ private static var userDefaults: UserDefaults? {
+ guard let appGroupId = FileManager.appGroupId else {
+ wg_log(.error, staticMessage: "Cannot obtain app group ID from bundle for tracking recently used tunnels")
+ return nil
+ }
+ guard let userDefaults = UserDefaults(suiteName: appGroupId) else {
+ wg_log(.error, staticMessage: "Cannot obtain shared user defaults for tracking recently used tunnels")
+ return nil
+ }
+ return userDefaults
+ }
+
+ static func handleTunnelActivated(tunnelName: String) {
+ guard let userDefaults = RecentTunnelsTracker.userDefaults else { return }
+ var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
+ if let existingIndex = recentTunnels.firstIndex(of: tunnelName) {
+ recentTunnels.remove(at: existingIndex)
+ }
+ recentTunnels.insert(tunnelName, at: 0)
+ if recentTunnels.count > maxNumberOfTunnels {
+ recentTunnels.removeLast(recentTunnels.count - maxNumberOfTunnels)
+ }
+ userDefaults.set(recentTunnels, forKey: keyRecentlyActivatedTunnelNames)
+ }
+
+ static func handleTunnelRemoved(tunnelName: String) {
+ guard let userDefaults = RecentTunnelsTracker.userDefaults else { return }
+ var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
+ if let existingIndex = recentTunnels.firstIndex(of: tunnelName) {
+ recentTunnels.remove(at: existingIndex)
+ userDefaults.set(recentTunnels, forKey: keyRecentlyActivatedTunnelNames)
+ }
+ }
+
+ static func handleTunnelRenamed(oldName: String, newName: String) {
+ guard let userDefaults = RecentTunnelsTracker.userDefaults else { return }
+ var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
+ if let existingIndex = recentTunnels.firstIndex(of: oldName) {
+ recentTunnels[existingIndex] = newName
+ userDefaults.set(recentTunnels, forKey: keyRecentlyActivatedTunnelNames)
+ }
+ }
+
+ static func cleanupTunnels(except tunnelNamesToKeep: Set<String>) {
+ guard let userDefaults = RecentTunnelsTracker.userDefaults else { return }
+ var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
+ let oldCount = recentTunnels.count
+ recentTunnels.removeAll { !tunnelNamesToKeep.contains($0) }
+ if oldCount != recentTunnels.count {
+ userDefaults.set(recentTunnels, forKey: keyRecentlyActivatedTunnelNames)
+ }
+ }
+
+ static func recentlyActivatedTunnelNames(limit: Int) -> [String] {
+ guard let userDefaults = RecentTunnelsTracker.userDefaults else { return [] }
+ var recentTunnels = userDefaults.stringArray(forKey: keyRecentlyActivatedTunnelNames) ?? []
+ if limit < recentTunnels.count {
+ recentTunnels.removeLast(recentTunnels.count - limit)
+ }
+ return recentTunnels
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/UITableViewCell+Reuse.swift b/Sources/WireGuardApp/UI/iOS/UITableViewCell+Reuse.swift
new file mode 100644
index 0000000..bee1170
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/UITableViewCell+Reuse.swift
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+extension UITableViewCell {
+ static var reuseIdentifier: String {
+ return NSStringFromClass(self)
+ }
+}
+
+extension UITableView {
+ func register<T: UITableViewCell>(_: T.Type) {
+ register(T.self, forCellReuseIdentifier: T.reuseIdentifier)
+ }
+
+ func dequeueReusableCell<T: UITableViewCell>(for indexPath: IndexPath) -> T {
+ //swiftlint:disable:next force_cast
+ return dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as! T
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/View/BorderedTextButton.swift b/Sources/WireGuardApp/UI/iOS/View/BorderedTextButton.swift
new file mode 100644
index 0000000..82e7b2d
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/View/BorderedTextButton.swift
@@ -0,0 +1,51 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class BorderedTextButton: UIView {
+ let button: UIButton = {
+ let button = UIButton(type: .system)
+ button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
+ button.titleLabel?.adjustsFontForContentSizeCategory = true
+ return button
+ }()
+
+ override var intrinsicContentSize: CGSize {
+ let buttonSize = button.intrinsicContentSize
+ return CGSize(width: buttonSize.width + 32, height: buttonSize.height + 16)
+ }
+
+ var title: String {
+ get { return button.title(for: .normal) ?? "" }
+ set(value) { button.setTitle(value, for: .normal) }
+ }
+
+ var onTapped: (() -> Void)?
+
+ init() {
+ super.init(frame: CGRect.zero)
+
+ layer.borderWidth = 1
+ layer.cornerRadius = 5
+ layer.borderColor = button.tintColor.cgColor
+
+ addSubview(button)
+ button.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ button.centerXAnchor.constraint(equalTo: centerXAnchor),
+ button.centerYAnchor.constraint(equalTo: centerYAnchor)
+ ])
+
+ button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc func buttonTapped() {
+ onTapped?()
+ }
+
+}
diff --git a/Sources/WireGuardApp/UI/iOS/View/ButtonCell.swift b/Sources/WireGuardApp/UI/iOS/View/ButtonCell.swift
new file mode 100644
index 0000000..90601c5
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/View/ButtonCell.swift
@@ -0,0 +1,55 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class ButtonCell: UITableViewCell {
+ var buttonText: String {
+ get { return button.title(for: .normal) ?? "" }
+ set(value) { button.setTitle(value, for: .normal) }
+ }
+ var hasDestructiveAction: Bool {
+ get { return button.tintColor == .systemRed }
+ set(value) { button.tintColor = value ? .systemRed : buttonStandardTintColor }
+ }
+ var onTapped: (() -> Void)?
+
+ let button: UIButton = {
+ let button = UIButton(type: .system)
+ button.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
+ button.titleLabel?.adjustsFontForContentSizeCategory = true
+ return button
+ }()
+
+ var buttonStandardTintColor: UIColor
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ buttonStandardTintColor = button.tintColor
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ contentView.addSubview(button)
+ button.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ button.topAnchor.constraint(equalTo: contentView.layoutMarginsGuide.topAnchor),
+ contentView.layoutMarginsGuide.bottomAnchor.constraint(equalTo: button.bottomAnchor),
+ button.centerXAnchor.constraint(equalTo: contentView.centerXAnchor)
+ ])
+
+ button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
+ }
+
+ @objc func buttonTapped() {
+ onTapped?()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ buttonText = ""
+ onTapped = nil
+ hasDestructiveAction = false
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/View/CheckmarkCell.swift b/Sources/WireGuardApp/UI/iOS/View/CheckmarkCell.swift
new file mode 100644
index 0000000..3fdf1b9
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/View/CheckmarkCell.swift
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class CheckmarkCell: UITableViewCell {
+ var message: String {
+ get { return textLabel?.text ?? "" }
+ set(value) { textLabel!.text = value }
+ }
+ var isChecked: Bool {
+ didSet {
+ accessoryType = isChecked ? .checkmark : .none
+ }
+ }
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ isChecked = false
+ super.init(style: .default, reuseIdentifier: reuseIdentifier)
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ message = ""
+ isChecked = false
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/View/ChevronCell.swift b/Sources/WireGuardApp/UI/iOS/View/ChevronCell.swift
new file mode 100644
index 0000000..a059b9c
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/View/ChevronCell.swift
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class ChevronCell: UITableViewCell {
+ var message: String {
+ get { return textLabel?.text ?? "" }
+ set(value) { textLabel?.text = value }
+ }
+
+ var detailMessage: String {
+ get { return detailTextLabel?.text ?? "" }
+ set(value) { detailTextLabel?.text = value }
+ }
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: .value1, reuseIdentifier: reuseIdentifier)
+ accessoryType = .disclosureIndicator
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ message = ""
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/View/EditableTextCell.swift b/Sources/WireGuardApp/UI/iOS/View/EditableTextCell.swift
new file mode 100644
index 0000000..178b200
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/View/EditableTextCell.swift
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class EditableTextCell: UITableViewCell {
+ var message: String {
+ get { return valueTextField.text ?? "" }
+ set(value) { valueTextField.text = value }
+ }
+
+ let valueTextField: UITextField = {
+ let valueTextField = UITextField()
+ valueTextField.textAlignment = .left
+ valueTextField.isEnabled = true
+ valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
+ valueTextField.adjustsFontForContentSizeCategory = true
+ valueTextField.autocapitalizationType = .none
+ valueTextField.autocorrectionType = .no
+ valueTextField.spellCheckingType = .no
+ return valueTextField
+ }()
+
+ var onValueBeingEdited: ((String) -> Void)?
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ valueTextField.delegate = self
+ contentView.addSubview(valueTextField)
+ valueTextField.translatesAutoresizingMaskIntoConstraints = false
+ let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueTextField.bottomAnchor, multiplier: 1)
+ bottomAnchorConstraint.priority = .defaultLow
+ NSLayoutConstraint.activate([
+ valueTextField.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1),
+ contentView.layoutMarginsGuide.trailingAnchor.constraint(equalToSystemSpacingAfter: valueTextField.trailingAnchor, multiplier: 1),
+ valueTextField.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
+ bottomAnchorConstraint
+ ])
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func beginEditing() {
+ valueTextField.becomeFirstResponder()
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ message = ""
+ }
+}
+
+extension EditableTextCell: UITextFieldDelegate {
+ func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
+ if let onValueBeingEdited = onValueBeingEdited {
+ let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
+ onValueBeingEdited(modifiedText)
+ }
+ return true
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/View/KeyValueCell.swift b/Sources/WireGuardApp/UI/iOS/View/KeyValueCell.swift
new file mode 100644
index 0000000..9b1d13b
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/View/KeyValueCell.swift
@@ -0,0 +1,236 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class KeyValueCell: UITableViewCell {
+
+ let keyLabel: UILabel = {
+ let keyLabel = UILabel()
+ keyLabel.font = UIFont.preferredFont(forTextStyle: .body)
+ keyLabel.adjustsFontForContentSizeCategory = true
+ if #available(iOS 13.0, *) {
+ keyLabel.textColor = .label
+ } else {
+ keyLabel.textColor = .black
+ }
+ keyLabel.textAlignment = .left
+ return keyLabel
+ }()
+
+ let valueLabelScrollView: UIScrollView = {
+ let scrollView = UIScrollView(frame: .zero)
+ scrollView.isDirectionalLockEnabled = true
+ scrollView.showsHorizontalScrollIndicator = false
+ scrollView.showsVerticalScrollIndicator = false
+ return scrollView
+ }()
+
+ let valueTextField: UITextField = {
+ let valueTextField = UITextField()
+ valueTextField.textAlignment = .right
+ valueTextField.isEnabled = false
+ valueTextField.font = UIFont.preferredFont(forTextStyle: .body)
+ valueTextField.adjustsFontForContentSizeCategory = true
+ valueTextField.autocapitalizationType = .none
+ valueTextField.autocorrectionType = .no
+ valueTextField.spellCheckingType = .no
+ if #available(iOS 13.0, *) {
+ valueTextField.textColor = .secondaryLabel
+ } else {
+ valueTextField.textColor = .gray
+ }
+ return valueTextField
+ }()
+
+ var copyableGesture = true
+
+ var key: String {
+ get { return keyLabel.text ?? "" }
+ set(value) { keyLabel.text = value }
+ }
+ var value: String {
+ get { return valueTextField.text ?? "" }
+ set(value) { valueTextField.text = value }
+ }
+ var placeholderText: String {
+ get { return valueTextField.placeholder ?? "" }
+ set(value) { valueTextField.placeholder = value }
+ }
+ var keyboardType: UIKeyboardType {
+ get { return valueTextField.keyboardType }
+ set(value) { valueTextField.keyboardType = value }
+ }
+
+ var isValueValid = true {
+ didSet {
+ if #available(iOS 13.0, *) {
+ if isValueValid {
+ keyLabel.textColor = .label
+ } else {
+ keyLabel.textColor = .systemRed
+ }
+ } else {
+ if isValueValid {
+ keyLabel.textColor = .black
+ } else {
+ keyLabel.textColor = .red
+ }
+ }
+ }
+ }
+
+ var isStackedHorizontally = false
+ var isStackedVertically = false
+ var contentSizeBasedConstraints = [NSLayoutConstraint]()
+
+ var onValueChanged: ((String, String) -> Void)?
+ var onValueBeingEdited: ((String) -> Void)?
+
+ var observationToken: AnyObject?
+
+ private var textFieldValueOnBeginEditing: String = ""
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ contentView.addSubview(keyLabel)
+ keyLabel.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ keyLabel.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
+ keyLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
+ ])
+
+ valueTextField.delegate = self
+ valueLabelScrollView.addSubview(valueTextField)
+ valueTextField.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ valueTextField.leadingAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.leadingAnchor),
+ valueTextField.topAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.topAnchor),
+ valueTextField.bottomAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.bottomAnchor),
+ valueTextField.trailingAnchor.constraint(equalTo: valueLabelScrollView.contentLayoutGuide.trailingAnchor),
+ valueTextField.heightAnchor.constraint(equalTo: valueLabelScrollView.heightAnchor)
+ ])
+ let expandToFitValueLabelConstraint = NSLayoutConstraint(item: valueTextField, attribute: .width, relatedBy: .equal, toItem: valueLabelScrollView, attribute: .width, multiplier: 1, constant: 0)
+ expandToFitValueLabelConstraint.priority = .defaultLow + 1
+ expandToFitValueLabelConstraint.isActive = true
+
+ contentView.addSubview(valueLabelScrollView)
+
+ contentView.addSubview(valueLabelScrollView)
+ valueLabelScrollView.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ valueLabelScrollView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
+ contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: valueLabelScrollView.bottomAnchor, multiplier: 0.5)
+ ])
+
+ keyLabel.setContentCompressionResistancePriority(.defaultHigh + 1, for: .horizontal)
+ keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+ valueLabelScrollView.setContentHuggingPriority(.defaultLow, for: .horizontal)
+
+ let gestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTapGesture(_:)))
+ addGestureRecognizer(gestureRecognizer)
+ isUserInteractionEnabled = true
+
+ configureForContentSize()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func configureForContentSize() {
+ var constraints = [NSLayoutConstraint]()
+ if traitCollection.preferredContentSizeCategory.isAccessibilityCategory {
+ // Stack vertically
+ if !isStackedVertically {
+ constraints = [
+ valueLabelScrollView.topAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
+ valueLabelScrollView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
+ keyLabel.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor)
+ ]
+ isStackedVertically = true
+ isStackedHorizontally = false
+ }
+ } else {
+ // Stack horizontally
+ if !isStackedHorizontally {
+ constraints = [
+ contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: keyLabel.bottomAnchor, multiplier: 0.5),
+ valueLabelScrollView.leadingAnchor.constraint(equalToSystemSpacingAfter: keyLabel.trailingAnchor, multiplier: 1),
+ valueLabelScrollView.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 0.5)
+ ]
+ isStackedHorizontally = true
+ isStackedVertically = false
+ }
+ }
+ if !constraints.isEmpty {
+ NSLayoutConstraint.deactivate(contentSizeBasedConstraints)
+ NSLayoutConstraint.activate(constraints)
+ contentSizeBasedConstraints = constraints
+ }
+ }
+
+ @objc func handleTapGesture(_ recognizer: UIGestureRecognizer) {
+ if !copyableGesture {
+ return
+ }
+ guard recognizer.state == .recognized else { return }
+
+ if let recognizerView = recognizer.view,
+ let recognizerSuperView = recognizerView.superview, recognizerView.becomeFirstResponder() {
+ let menuController = UIMenuController.shared
+ menuController.setTargetRect(detailTextLabel?.frame ?? recognizerView.frame, in: detailTextLabel?.superview ?? recognizerSuperView)
+ menuController.setMenuVisible(true, animated: true)
+ }
+ }
+
+ override var canBecomeFirstResponder: Bool {
+ return true
+ }
+
+ override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
+ return (action == #selector(UIResponderStandardEditActions.copy(_:)))
+ }
+
+ override func copy(_ sender: Any?) {
+ UIPasteboard.general.string = valueTextField.text
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ copyableGesture = true
+ placeholderText = ""
+ isValueValid = true
+ keyboardType = .default
+ onValueChanged = nil
+ onValueBeingEdited = nil
+ observationToken = nil
+ key = ""
+ value = ""
+ configureForContentSize()
+ }
+}
+
+extension KeyValueCell: UITextFieldDelegate {
+
+ func textFieldDidBeginEditing(_ textField: UITextField) {
+ textFieldValueOnBeginEditing = textField.text ?? ""
+ isValueValid = true
+ }
+
+ func textFieldDidEndEditing(_ textField: UITextField) {
+ let isModified = textField.text ?? "" != textFieldValueOnBeginEditing
+ guard isModified else { return }
+ onValueChanged?(textFieldValueOnBeginEditing, textField.text ?? "")
+ }
+
+ func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
+ if let onValueBeingEdited = onValueBeingEdited {
+ let modifiedText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
+ onValueBeingEdited(modifiedText)
+ }
+ return true
+ }
+
+}
diff --git a/Sources/WireGuardApp/UI/iOS/View/SwitchCell.swift b/Sources/WireGuardApp/UI/iOS/View/SwitchCell.swift
new file mode 100644
index 0000000..0d6e4de
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/View/SwitchCell.swift
@@ -0,0 +1,56 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class SwitchCell: UITableViewCell {
+ var message: String {
+ get { return textLabel?.text ?? "" }
+ set(value) { textLabel?.text = value }
+ }
+ var isOn: Bool {
+ get { return switchView.isOn }
+ set(value) { switchView.isOn = value }
+ }
+ var isEnabled: Bool {
+ get { return switchView.isEnabled }
+ set(value) {
+ switchView.isEnabled = value
+ if #available(iOS 13.0, *) {
+ textLabel?.textColor = value ? .label : .secondaryLabel
+ } else {
+ textLabel?.textColor = value ? .black : .gray
+ }
+ }
+ }
+
+ var onSwitchToggled: ((Bool) -> Void)?
+
+ var observationToken: AnyObject?
+
+ let switchView = UISwitch()
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: .default, reuseIdentifier: reuseIdentifier)
+
+ accessoryView = switchView
+ switchView.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc func switchToggled() {
+ onSwitchToggled?(switchView.isOn)
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ onSwitchToggled = nil
+ isEnabled = true
+ message = ""
+ isOn = false
+ observationToken = nil
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/View/TextCell.swift b/Sources/WireGuardApp/UI/iOS/View/TextCell.swift
new file mode 100644
index 0000000..7df9444
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/View/TextCell.swift
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class TextCell: UITableViewCell {
+ var message: String {
+ get { return textLabel?.text ?? "" }
+ set(value) { textLabel!.text = value }
+ }
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: .default, reuseIdentifier: reuseIdentifier)
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func setTextColor(_ color: UIColor) {
+ textLabel?.textColor = color
+ }
+
+ func setTextAlignment(_ alignment: NSTextAlignment) {
+ textLabel?.textAlignment = alignment
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ message = ""
+ if #available(iOS 13.0, *) {
+ setTextColor(.label)
+ } else {
+ setTextColor(.black)
+ }
+ setTextAlignment(.left)
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/View/TunnelEditKeyValueCell.swift b/Sources/WireGuardApp/UI/iOS/View/TunnelEditKeyValueCell.swift
new file mode 100644
index 0000000..c139566
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/View/TunnelEditKeyValueCell.swift
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class TunnelEditKeyValueCell: KeyValueCell {
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ keyLabel.textAlignment = .right
+ valueTextField.textAlignment = .left
+
+ let widthRatioConstraint = NSLayoutConstraint(item: keyLabel, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 0.4, constant: 0)
+ // In case the key doesn't fit into 0.4 * width,
+ // set a CR priority > the 0.4-constraint's priority.
+ widthRatioConstraint.priority = .defaultHigh + 1
+ widthRatioConstraint.isActive = true
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+}
+
+class TunnelEditEditableKeyValueCell: TunnelEditKeyValueCell {
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ copyableGesture = false
+ if #available(iOS 13.0, *) {
+ valueTextField.textColor = .label
+ } else {
+ valueTextField.textColor = .black
+ }
+ valueTextField.isEnabled = true
+ valueLabelScrollView.isScrollEnabled = false
+ valueTextField.widthAnchor.constraint(equalTo: valueLabelScrollView.widthAnchor).isActive = true
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ copyableGesture = false
+ }
+
+}
diff --git a/Sources/WireGuardApp/UI/iOS/View/TunnelListCell.swift b/Sources/WireGuardApp/UI/iOS/View/TunnelListCell.swift
new file mode 100644
index 0000000..b2e0ba9
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/View/TunnelListCell.swift
@@ -0,0 +1,122 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class TunnelListCell: UITableViewCell {
+ var tunnel: TunnelContainer? {
+ didSet(value) {
+ // Bind to the tunnel's name
+ nameLabel.text = tunnel?.name ?? ""
+ nameObservationToken = tunnel?.observe(\.name) { [weak self] tunnel, _ in
+ self?.nameLabel.text = tunnel.name
+ }
+ // Bind to the tunnel's status
+ update(from: tunnel?.status)
+ statusObservationToken = tunnel?.observe(\.status) { [weak self] tunnel, _ in
+ self?.update(from: tunnel.status)
+ }
+ }
+ }
+ var onSwitchToggled: ((Bool) -> Void)?
+
+ let nameLabel: UILabel = {
+ let nameLabel = UILabel()
+ nameLabel.font = UIFont.preferredFont(forTextStyle: .body)
+ nameLabel.adjustsFontForContentSizeCategory = true
+ nameLabel.numberOfLines = 0
+ return nameLabel
+ }()
+
+ let busyIndicator: UIActivityIndicatorView = {
+ if #available(iOS 13.0, *) {
+ let busyIndicator = UIActivityIndicatorView(style: .medium)
+ busyIndicator.hidesWhenStopped = true
+ return busyIndicator
+ } else {
+ let busyIndicator = UIActivityIndicatorView(style: .gray)
+ busyIndicator.hidesWhenStopped = true
+ return busyIndicator
+ }
+ }()
+
+ let statusSwitch = UISwitch()
+
+ private var statusObservationToken: AnyObject?
+ private var nameObservationToken: AnyObject?
+
+ override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
+ super.init(style: style, reuseIdentifier: reuseIdentifier)
+
+ contentView.addSubview(statusSwitch)
+ statusSwitch.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ statusSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
+ contentView.trailingAnchor.constraint(equalTo: statusSwitch.trailingAnchor)
+ ])
+
+ contentView.addSubview(busyIndicator)
+ busyIndicator.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ busyIndicator.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
+ statusSwitch.leadingAnchor.constraint(equalToSystemSpacingAfter: busyIndicator.trailingAnchor, multiplier: 1)
+ ])
+
+ contentView.addSubview(nameLabel)
+ nameLabel.translatesAutoresizingMaskIntoConstraints = false
+ nameLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
+ let bottomAnchorConstraint = contentView.layoutMarginsGuide.bottomAnchor.constraint(equalToSystemSpacingBelow: nameLabel.bottomAnchor, multiplier: 1)
+ bottomAnchorConstraint.priority = .defaultLow
+ NSLayoutConstraint.activate([
+ nameLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.layoutMarginsGuide.topAnchor, multiplier: 1),
+ nameLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: contentView.layoutMarginsGuide.leadingAnchor, multiplier: 1),
+ busyIndicator.leadingAnchor.constraint(equalToSystemSpacingAfter: nameLabel.trailingAnchor, multiplier: 1),
+ bottomAnchorConstraint
+ ])
+
+ accessoryType = .disclosureIndicator
+
+ statusSwitch.addTarget(self, action: #selector(switchToggled), for: .valueChanged)
+ }
+
+ @objc func switchToggled() {
+ onSwitchToggled?(statusSwitch.isOn)
+ }
+
+ private func update(from status: TunnelStatus?) {
+ guard let status = status else {
+ reset()
+ return
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak statusSwitch, weak busyIndicator] in
+ guard let statusSwitch = statusSwitch, let busyIndicator = busyIndicator else { return }
+ statusSwitch.isOn = !(status == .deactivating || status == .inactive)
+ statusSwitch.isUserInteractionEnabled = (status == .inactive || status == .active)
+ if status == .inactive || status == .active {
+ busyIndicator.stopAnimating()
+ } else {
+ busyIndicator.startAnimating()
+ }
+ }
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func setEditing(_ editing: Bool, animated: Bool) {
+ super.setEditing(editing, animated: animated)
+ statusSwitch.isEnabled = !editing
+ }
+
+ private func reset() {
+ statusSwitch.isOn = false
+ statusSwitch.isUserInteractionEnabled = false
+ busyIndicator.stopAnimating()
+ }
+
+ override func prepareForReuse() {
+ super.prepareForReuse()
+ reset()
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/ViewController/LogViewController.swift b/Sources/WireGuardApp/UI/iOS/ViewController/LogViewController.swift
new file mode 100644
index 0000000..f31af01
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/ViewController/LogViewController.swift
@@ -0,0 +1,165 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class LogViewController: UIViewController {
+
+ let textView: UITextView = {
+ let textView = UITextView()
+ textView.isEditable = false
+ textView.isSelectable = true
+ textView.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)
+ textView.adjustsFontForContentSizeCategory = true
+ return textView
+ }()
+
+ let busyIndicator: UIActivityIndicatorView = {
+ if #available(iOS 13.0, *) {
+ let busyIndicator = UIActivityIndicatorView(style: .medium)
+ busyIndicator.hidesWhenStopped = true
+ return busyIndicator
+ } else {
+ let busyIndicator = UIActivityIndicatorView(style: .gray)
+ busyIndicator.hidesWhenStopped = true
+ return busyIndicator
+ }
+ }()
+
+ let paragraphStyle: NSParagraphStyle = {
+ let paragraphStyle = NSMutableParagraphStyle()
+ paragraphStyle.setParagraphStyle(NSParagraphStyle.default)
+ paragraphStyle.lineHeightMultiple = 1.2
+ return paragraphStyle
+ }()
+
+ var isNextLineHighlighted = false
+
+ var logViewHelper: LogViewHelper?
+ var isFetchingLogEntries = false
+ private var updateLogEntriesTimer: Timer?
+
+ override func loadView() {
+ view = UIView()
+ if #available(iOS 13.0, *) {
+ view.backgroundColor = .systemBackground
+ } else {
+ view.backgroundColor = .white
+ }
+
+ view.addSubview(textView)
+ textView.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ textView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ textView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ textView.topAnchor.constraint(equalTo: view.topAnchor),
+ textView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
+ ])
+
+ view.addSubview(busyIndicator)
+ busyIndicator.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
+ ])
+
+ busyIndicator.startAnimating()
+
+ logViewHelper = LogViewHelper(logFilePath: FileManager.logFileURL?.path)
+ startUpdatingLogEntries()
+ }
+
+ override func viewDidLoad() {
+ title = tr("logViewTitle")
+ navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped(sender:)))
+ }
+
+ func updateLogEntries() {
+ guard !isFetchingLogEntries else { return }
+ isFetchingLogEntries = true
+ logViewHelper?.fetchLogEntriesSinceLastFetch { [weak self] fetchedLogEntries in
+ guard let self = self else { return }
+ defer {
+ self.isFetchingLogEntries = false
+ }
+ if self.busyIndicator.isAnimating {
+ self.busyIndicator.stopAnimating()
+ }
+ guard !fetchedLogEntries.isEmpty else { return }
+ let isScrolledToEnd = self.textView.contentSize.height - self.textView.bounds.height - self.textView.contentOffset.y < 1
+
+ let richText = NSMutableAttributedString()
+ let bodyFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.body)
+ let captionFont = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.caption1)
+ for logEntry in fetchedLogEntries {
+ var bgColor: UIColor
+ var fgColor: UIColor
+ if #available(iOS 13.0, *) {
+ bgColor = self.isNextLineHighlighted ? .systemGray3 : .systemBackground
+ fgColor = .label
+ } else {
+ bgColor = self.isNextLineHighlighted ? UIColor(white: 0.88, alpha: 1.0) : UIColor.white
+ fgColor = .black
+ }
+ let timestampText = NSAttributedString(string: logEntry.timestamp + "\n", attributes: [.font: captionFont, .backgroundColor: bgColor, .foregroundColor: fgColor, .paragraphStyle: self.paragraphStyle])
+ let messageText = NSAttributedString(string: logEntry.message + "\n", attributes: [.font: bodyFont, .backgroundColor: bgColor, .foregroundColor: fgColor, .paragraphStyle: self.paragraphStyle])
+ richText.append(timestampText)
+ richText.append(messageText)
+ self.isNextLineHighlighted.toggle()
+ }
+ self.textView.textStorage.append(richText)
+ if isScrolledToEnd {
+ let endOfCurrentText = NSRange(location: (self.textView.text as NSString).length, length: 0)
+ self.textView.scrollRangeToVisible(endOfCurrentText)
+ }
+ }
+ }
+
+ func startUpdatingLogEntries() {
+ updateLogEntries()
+ updateLogEntriesTimer?.invalidate()
+ let timer = Timer(timeInterval: 1 /* second */, repeats: true) { [weak self] _ in
+ self?.updateLogEntries()
+ }
+ updateLogEntriesTimer = timer
+ RunLoop.main.add(timer, forMode: .common)
+ }
+
+ @objc func saveTapped(sender: AnyObject) {
+ guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
+
+ let dateFormatter = ISO8601DateFormatter()
+ dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename
+ let timeStampString = dateFormatter.string(from: Date())
+ let destinationURL = destinationDir.appendingPathComponent("wireguard-log-\(timeStampString).txt")
+
+ DispatchQueue.global(qos: .userInitiated).async {
+
+ if FileManager.default.fileExists(atPath: destinationURL.path) {
+ let isDeleted = FileManager.deleteFile(at: destinationURL)
+ if !isDeleted {
+ ErrorPresenter.showErrorAlert(title: tr("alertUnableToRemovePreviousLogTitle"), message: tr("alertUnableToRemovePreviousLogMessage"), from: self)
+ return
+ }
+ }
+
+ let isWritten = Logger.global?.writeLog(to: destinationURL.path) ?? false
+
+ DispatchQueue.main.async {
+ guard isWritten else {
+ ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
+ return
+ }
+ let activityVC = UIActivityViewController(activityItems: [destinationURL], applicationActivities: nil)
+ if let sender = sender as? UIBarButtonItem {
+ activityVC.popoverPresentationController?.barButtonItem = sender
+ }
+ activityVC.completionWithItemsHandler = { _, _, _, _ in
+ // Remove the exported log file after the activity has completed
+ _ = FileManager.deleteFile(at: destinationURL)
+ }
+ self.present(activityVC, animated: true)
+ }
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/ViewController/MainViewController.swift b/Sources/WireGuardApp/UI/iOS/ViewController/MainViewController.swift
new file mode 100644
index 0000000..514e037
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/ViewController/MainViewController.swift
@@ -0,0 +1,146 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class MainViewController: UISplitViewController {
+
+ var tunnelsManager: TunnelsManager?
+ var onTunnelsManagerReady: ((TunnelsManager) -> Void)?
+ var tunnelsListVC: TunnelsListTableViewController?
+
+ init() {
+ let detailVC = UIViewController()
+ if #available(iOS 13.0, *) {
+ detailVC.view.backgroundColor = .systemBackground
+ } else {
+ detailVC.view.backgroundColor = .white
+ }
+ let detailNC = UINavigationController(rootViewController: detailVC)
+
+ let masterVC = TunnelsListTableViewController()
+ let masterNC = UINavigationController(rootViewController: masterVC)
+
+ tunnelsListVC = masterVC
+
+ super.init(nibName: nil, bundle: nil)
+
+ viewControllers = [ masterNC, detailNC ]
+
+ restorationIdentifier = "MainVC"
+ masterNC.restorationIdentifier = "MasterNC"
+ detailNC.restorationIdentifier = "DetailNC"
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ delegate = self
+
+ // 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
+ }
+ }
+ }
+
+ func allTunnelNames() -> [String]? {
+ guard let tunnelsManager = self.tunnelsManager else { return nil }
+ return tunnelsManager.mapTunnels { $0.name }
+ }
+}
+
+extension MainViewController: TunnelsManagerActivationDelegate {
+ func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) {
+ ErrorPresenter.showErrorAlert(error: error, from: self)
+ }
+
+ func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) {
+ // Nothing to do
+ }
+
+ func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) {
+ ErrorPresenter.showErrorAlert(error: error, from: self)
+ }
+
+ func tunnelActivationSucceeded(tunnel: TunnelContainer) {
+ // Nothing to do
+ }
+}
+
+extension MainViewController {
+ func refreshTunnelConnectionStatuses() {
+ if let tunnelsManager = tunnelsManager {
+ tunnelsManager.refreshStatuses()
+ }
+ }
+
+ func showTunnelDetailForTunnel(named tunnelName: String, animated: Bool, shouldToggleStatus: Bool) {
+ let showTunnelDetailBlock: (TunnelsManager) -> Void = { [weak self] tunnelsManager in
+ guard let self = self else { return }
+ guard let tunnelsListVC = self.tunnelsListVC else { return }
+ if let tunnel = tunnelsManager.tunnel(named: tunnelName) {
+ tunnelsListVC.showTunnelDetail(for: tunnel, animated: false)
+ if shouldToggleStatus {
+ if tunnel.status == .inactive {
+ tunnelsManager.startActivation(of: tunnel)
+ } else if tunnel.status == .active {
+ tunnelsManager.startDeactivation(of: tunnel)
+ }
+ }
+ }
+ }
+ if let tunnelsManager = tunnelsManager {
+ showTunnelDetailBlock(tunnelsManager)
+ } else {
+ onTunnelsManagerReady = showTunnelDetailBlock
+ }
+ }
+
+ func importFromDisposableFile(url: URL) {
+ let importFromFileBlock: (TunnelsManager) -> Void = { [weak self] tunnelsManager in
+ TunnelImporter.importFromFile(urls: [url], into: tunnelsManager, sourceVC: self, errorPresenterType: ErrorPresenter.self) {
+ _ = FileManager.deleteFile(at: url)
+ }
+ }
+ if let tunnelsManager = tunnelsManager {
+ importFromFileBlock(tunnelsManager)
+ } else {
+ onTunnelsManagerReady = importFromFileBlock
+ }
+ }
+}
+
+extension MainViewController: UISplitViewControllerDelegate {
+ func splitViewController(_ splitViewController: UISplitViewController,
+ collapseSecondary secondaryViewController: UIViewController,
+ onto primaryViewController: UIViewController) -> Bool {
+ // On iPhone, if the secondaryVC (detailVC) is just a UIViewController, it indicates that it's empty,
+ // so just show the primaryVC (masterVC).
+ let detailVC = (secondaryViewController as? UINavigationController)?.viewControllers.first
+ let isDetailVCEmpty: Bool
+ if let detailVC = detailVC {
+ isDetailVCEmpty = (type(of: detailVC) == UIViewController.self)
+ } else {
+ isDetailVCEmpty = true
+ }
+ return isDetailVCEmpty
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/ViewController/QRScanViewController.swift b/Sources/WireGuardApp/UI/iOS/ViewController/QRScanViewController.swift
new file mode 100644
index 0000000..041421f
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/ViewController/QRScanViewController.swift
@@ -0,0 +1,156 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import AVFoundation
+import UIKit
+import WireGuardKit
+
+protocol QRScanViewControllerDelegate: class {
+ func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController, completionHandler: (() -> Void)?)
+}
+
+class QRScanViewController: UIViewController {
+ weak var delegate: QRScanViewControllerDelegate?
+ var captureSession: AVCaptureSession? = AVCaptureSession()
+ let metadataOutput = AVCaptureMetadataOutput()
+ var previewLayer: AVCaptureVideoPreviewLayer?
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ title = tr("scanQRCodeViewTitle")
+ navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
+
+ let tipLabel = UILabel()
+ tipLabel.text = tr("scanQRCodeTipText")
+ tipLabel.adjustsFontSizeToFitWidth = true
+ tipLabel.textColor = .lightGray
+ tipLabel.textAlignment = .center
+
+ view.addSubview(tipLabel)
+ tipLabel.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ tipLabel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
+ tipLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
+ tipLabel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -32)
+ ])
+
+ 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: tr("alertScanQRCodeCameraUnsupportedTitle"), message: tr("alertScanQRCodeCameraUnsupportedMessage"))
+ return
+ }
+
+ captureSession.addInput(videoInput)
+ captureSession.addOutput(metadataOutput)
+
+ metadataOutput.setMetadataObjectsDelegate(self, queue: .main)
+ metadataOutput.metadataObjectTypes = [.qr]
+
+ let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
+ previewLayer.frame = view.layer.bounds
+ previewLayer.videoGravity = .resizeAspectFill
+ view.layer.insertSublayer(previewLayer, at: 0)
+ self.previewLayer = 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()
+ }
+ }
+
+ override func viewDidLayoutSubviews() {
+ super.viewDidLayoutSubviews()
+
+ if let connection = previewLayer?.connection {
+ let currentDevice = UIDevice.current
+ let orientation = currentDevice.orientation
+ let previewLayerConnection = connection
+
+ if previewLayerConnection.isVideoOrientationSupported {
+ switch orientation {
+ case .portrait:
+ previewLayerConnection.videoOrientation = .portrait
+ case .landscapeRight:
+ previewLayerConnection.videoOrientation = .landscapeLeft
+ case .landscapeLeft:
+ previewLayerConnection.videoOrientation = .landscapeRight
+ case .portraitUpsideDown:
+ previewLayerConnection.videoOrientation = .portraitUpsideDown
+ default:
+ previewLayerConnection.videoOrientation = .portrait
+
+ }
+ }
+ }
+
+ previewLayer?.frame = view.bounds
+ }
+
+ func scanDidComplete(withCode code: String) {
+ let scannedTunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: code, called: "Scanned")
+ guard let tunnelConfiguration = scannedTunnelConfiguration else {
+ scanDidEncounterError(title: tr("alertScanQRCodeInvalidQRCodeTitle"), message: tr("alertScanQRCodeInvalidQRCodeMessage"))
+ return
+ }
+
+ let alert = UIAlertController(title: tr("alertScanQRCodeNamePromptTitle"), message: nil, preferredStyle: .alert)
+ alert.addTextField(configurationHandler: nil)
+ alert.addAction(UIAlertAction(title: tr("actionCancel"), style: .cancel) { [weak self] _ in
+ self?.dismiss(animated: true, completion: nil)
+ })
+ alert.addAction(UIAlertAction(title: tr("actionSave"), style: .default) { [weak self] _ in
+ guard let title = alert.textFields?[0].text?.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty else { return }
+ tunnelConfiguration.name = title
+ if let self = self {
+ self.delegate?.addScannedQRCode(tunnelConfiguration: tunnelConfiguration, qrScanViewController: self) {
+ self.dismiss(animated: true, completion: nil)
+ }
+ }
+ })
+ present(alert, animated: true)
+ }
+
+ func scanDidEncounterError(title: String, message: String) {
+ let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
+ alertController.addAction(UIAlertAction(title: tr("actionOK"), style: .default) { [weak self] _ in
+ self?.dismiss(animated: true, completion: nil)
+ })
+ present(alertController, animated: true)
+ captureSession = nil
+ }
+
+ @objc func cancelTapped() {
+ dismiss(animated: true, completion: 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: tr("alertScanQRCodeUnreadableQRCodeTitle"), message: tr("alertScanQRCodeUnreadableQRCodeMessage"))
+ return
+ }
+
+ AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
+ scanDidComplete(withCode: stringValue)
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/ViewController/SSIDOptionDetailTableViewController.swift b/Sources/WireGuardApp/UI/iOS/ViewController/SSIDOptionDetailTableViewController.swift
new file mode 100644
index 0000000..668797e
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/ViewController/SSIDOptionDetailTableViewController.swift
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+
+class SSIDOptionDetailTableViewController: UITableViewController {
+
+ let selectedSSIDs: [String]
+
+ init(title: String, ssids: [String]) {
+ selectedSSIDs = ssids
+ super.init(style: .grouped)
+ self.title = title
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ tableView.estimatedRowHeight = 44
+ tableView.rowHeight = UITableView.automaticDimension
+ tableView.allowsSelection = false
+
+ tableView.register(TextCell.self)
+ }
+}
+
+extension SSIDOptionDetailTableViewController {
+ override func numberOfSections(in tableView: UITableView) -> Int {
+ return 1
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return selectedSSIDs.count
+ }
+
+ override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ return tr("tunnelOnDemandSectionTitleSelectedSSIDs")
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.message = selectedSSIDs[indexPath.row]
+ return cell
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/ViewController/SSIDOptionEditTableViewController.swift b/Sources/WireGuardApp/UI/iOS/ViewController/SSIDOptionEditTableViewController.swift
new file mode 100644
index 0000000..a982258
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/ViewController/SSIDOptionEditTableViewController.swift
@@ -0,0 +1,303 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+import SystemConfiguration.CaptiveNetwork
+
+protocol SSIDOptionEditTableViewControllerDelegate: class {
+ func ssidOptionSaved(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String])
+}
+
+class SSIDOptionEditTableViewController: UITableViewController {
+ private enum Section {
+ case ssidOption
+ case selectedSSIDs
+ case addSSIDs
+ }
+
+ private enum AddSSIDRow {
+ case addConnectedSSID(connectedSSID: String)
+ case addNewSSID
+ }
+
+ weak var delegate: SSIDOptionEditTableViewControllerDelegate?
+
+ private var sections = [Section]()
+ private var addSSIDRows = [AddSSIDRow]()
+
+ let ssidOptionFields: [ActivateOnDemandViewModel.OnDemandSSIDOption] = [
+ .anySSID,
+ .onlySpecificSSIDs,
+ .exceptSpecificSSIDs
+ ]
+
+ var selectedOption: ActivateOnDemandViewModel.OnDemandSSIDOption
+ var selectedSSIDs: [String]
+ var connectedSSID: String?
+
+ init(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String]) {
+ selectedOption = option
+ selectedSSIDs = ssids
+ super.init(style: .grouped)
+ connectedSSID = getConnectedSSID()
+ loadSections()
+ loadAddSSIDRows()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ title = tr("tunnelOnDemandSSIDViewTitle")
+
+ tableView.estimatedRowHeight = 44
+ tableView.rowHeight = UITableView.automaticDimension
+
+ tableView.register(CheckmarkCell.self)
+ tableView.register(EditableTextCell.self)
+ tableView.register(TextCell.self)
+ tableView.isEditing = true
+ tableView.allowsSelectionDuringEditing = true
+ }
+
+ func loadSections() {
+ sections.removeAll()
+ sections.append(.ssidOption)
+ if selectedOption != .anySSID {
+ sections.append(.selectedSSIDs)
+ sections.append(.addSSIDs)
+ }
+ }
+
+ func loadAddSSIDRows() {
+ addSSIDRows.removeAll()
+ if let connectedSSID = connectedSSID {
+ if !selectedSSIDs.contains(connectedSSID) {
+ addSSIDRows.append(.addConnectedSSID(connectedSSID: connectedSSID))
+ }
+ }
+ addSSIDRows.append(.addNewSSID)
+ }
+
+ func updateTableViewAddSSIDRows() {
+ guard let addSSIDSection = sections.firstIndex(of: .addSSIDs) else { return }
+ let numberOfAddSSIDRows = addSSIDRows.count
+ let numberOfAddSSIDRowsInTableView = tableView.numberOfRows(inSection: addSSIDSection)
+ switch (numberOfAddSSIDRowsInTableView, numberOfAddSSIDRows) {
+ case (1, 2):
+ tableView.insertRows(at: [IndexPath(row: 0, section: addSSIDSection)], with: .automatic)
+ case (2, 1):
+ tableView.deleteRows(at: [IndexPath(row: 0, section: addSSIDSection)], with: .automatic)
+ default:
+ break
+ }
+ }
+
+ override func viewWillDisappear(_ animated: Bool) {
+ delegate?.ssidOptionSaved(option: selectedOption, ssids: selectedSSIDs)
+ }
+}
+
+extension SSIDOptionEditTableViewController {
+ override func numberOfSections(in tableView: UITableView) -> Int {
+ return sections.count
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch sections[section] {
+ case .ssidOption:
+ return ssidOptionFields.count
+ case .selectedSSIDs:
+ return selectedSSIDs.isEmpty ? 1 : selectedSSIDs.count
+ case .addSSIDs:
+ return addSSIDRows.count
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ switch sections[indexPath.section] {
+ case .ssidOption:
+ return ssidOptionCell(for: tableView, at: indexPath)
+ case .selectedSSIDs:
+ if !selectedSSIDs.isEmpty {
+ return selectedSSIDCell(for: tableView, at: indexPath)
+ } else {
+ return noSSIDsCell(for: tableView, at: indexPath)
+ }
+ case .addSSIDs:
+ return addSSIDCell(for: tableView, at: indexPath)
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
+ switch sections[indexPath.section] {
+ case .ssidOption:
+ return false
+ case .selectedSSIDs:
+ return !selectedSSIDs.isEmpty
+ case .addSSIDs:
+ return true
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
+ switch sections[indexPath.section] {
+ case .ssidOption:
+ return .none
+ case .selectedSSIDs:
+ return .delete
+ case .addSSIDs:
+ return .insert
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ switch sections[section] {
+ case .ssidOption:
+ return nil
+ case .selectedSSIDs:
+ return tr("tunnelOnDemandSectionTitleSelectedSSIDs")
+ case .addSSIDs:
+ return tr("tunnelOnDemandSectionTitleAddSSIDs")
+ }
+ }
+
+ private func ssidOptionCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ let field = ssidOptionFields[indexPath.row]
+ let cell: CheckmarkCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.message = field.localizedUIString
+ cell.isChecked = selectedOption == field
+ cell.isEditing = false
+ return cell
+ }
+
+ private func noSSIDsCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.message = tr("tunnelOnDemandNoSSIDs")
+ if #available(iOS 13.0, *) {
+ cell.setTextColor(.secondaryLabel)
+ } else {
+ cell.setTextColor(.gray)
+ }
+ cell.setTextAlignment(.center)
+ return cell
+ }
+
+ private func selectedSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ let cell: EditableTextCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.message = selectedSSIDs[indexPath.row]
+ cell.isEditing = true
+ cell.onValueBeingEdited = { [weak self, weak cell] text in
+ guard let self = self, let cell = cell else { return }
+ if let row = self.tableView.indexPath(for: cell)?.row {
+ self.selectedSSIDs[row] = text
+ self.loadAddSSIDRows()
+ self.updateTableViewAddSSIDRows()
+ }
+ }
+ return cell
+ }
+
+ private func addSSIDCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ let cell: TextCell = tableView.dequeueReusableCell(for: indexPath)
+ switch addSSIDRows[indexPath.row] {
+ case .addConnectedSSID:
+ cell.message = tr(format: "tunnelOnDemandAddMessageAddConnectedSSID (%@)", connectedSSID!)
+ case .addNewSSID:
+ cell.message = tr("tunnelOnDemandAddMessageAddNewSSID")
+ }
+ cell.isEditing = true
+ return cell
+ }
+
+ override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
+ switch sections[indexPath.section] {
+ case .ssidOption:
+ assertionFailure()
+ case .selectedSSIDs:
+ assert(editingStyle == .delete)
+ selectedSSIDs.remove(at: indexPath.row)
+ if !selectedSSIDs.isEmpty {
+ tableView.deleteRows(at: [indexPath], with: .automatic)
+ } else {
+ tableView.reloadRows(at: [indexPath], with: .automatic)
+ }
+ loadAddSSIDRows()
+ updateTableViewAddSSIDRows()
+ case .addSSIDs:
+ assert(editingStyle == .insert)
+ let newSSID: String
+ switch addSSIDRows[indexPath.row] {
+ case .addConnectedSSID(let connectedSSID):
+ newSSID = connectedSSID
+ case .addNewSSID:
+ newSSID = ""
+ }
+ selectedSSIDs.append(newSSID)
+ loadSections()
+ let selectedSSIDsSection = sections.firstIndex(of: .selectedSSIDs)!
+ let indexPath = IndexPath(row: selectedSSIDs.count - 1, section: selectedSSIDsSection)
+ if selectedSSIDs.count == 1 {
+ tableView.reloadRows(at: [indexPath], with: .automatic)
+ } else {
+ tableView.insertRows(at: [indexPath], with: .automatic)
+ }
+ loadAddSSIDRows()
+ updateTableViewAddSSIDRows()
+ if newSSID.isEmpty {
+ if let selectedSSIDCell = tableView.cellForRow(at: indexPath) as? EditableTextCell {
+ selectedSSIDCell.beginEditing()
+ }
+ }
+ }
+ }
+}
+
+extension SSIDOptionEditTableViewController {
+ override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
+ switch sections[indexPath.section] {
+ case .ssidOption:
+ return indexPath
+ case .selectedSSIDs, .addSSIDs:
+ return nil
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ switch sections[indexPath.section] {
+ case .ssidOption:
+ let previousOption = selectedOption
+ selectedOption = ssidOptionFields[indexPath.row]
+ guard previousOption != selectedOption else {
+ tableView.deselectRow(at: indexPath, animated: true)
+ return
+ }
+ loadSections()
+ if previousOption == .anySSID {
+ let indexSet = IndexSet(1 ... 2)
+ tableView.insertSections(indexSet, with: .fade)
+ }
+ if selectedOption == .anySSID {
+ let indexSet = IndexSet(1 ... 2)
+ tableView.deleteSections(indexSet, with: .fade)
+ }
+ tableView.reloadSections(IndexSet(integer: indexPath.section), with: .none)
+ case .selectedSSIDs, .addSSIDs:
+ assertionFailure()
+ }
+ }
+}
+
+private func getConnectedSSID() -> String? {
+ guard let supportedInterfaces = CNCopySupportedInterfaces() as? [CFString] else { return nil }
+ for interface in supportedInterfaces {
+ if let networkInfo = CNCopyCurrentNetworkInfo(interface) {
+ if let ssid = (networkInfo as NSDictionary)[kCNNetworkInfoKeySSID as String] as? String {
+ return !ssid.isEmpty ? ssid : nil
+ }
+ }
+ }
+ return nil
+}
diff --git a/Sources/WireGuardApp/UI/iOS/ViewController/SettingsTableViewController.swift b/Sources/WireGuardApp/UI/iOS/ViewController/SettingsTableViewController.swift
new file mode 100644
index 0000000..d2c6f0c
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/ViewController/SettingsTableViewController.swift
@@ -0,0 +1,185 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+import os.log
+import WireGuardKit
+
+class SettingsTableViewController: UITableViewController {
+
+ enum SettingsFields {
+ case iosAppVersion
+ case goBackendVersion
+ case exportZipArchive
+ case viewLog
+ case donateLink
+
+ var localizedUIString: String {
+ switch self {
+ case .iosAppVersion: return tr("settingsVersionKeyWireGuardForIOS")
+ case .goBackendVersion: return tr("settingsVersionKeyWireGuardGoBackend")
+ case .exportZipArchive: return tr("settingsExportZipButtonTitle")
+ case .viewLog: return tr("settingsViewLogButtonTitle")
+ case .donateLink: return tr("donateLink")
+ }
+ }
+ }
+
+ let settingsFieldsBySection: [[SettingsFields]] = [
+ [.iosAppVersion, .goBackendVersion, .donateLink],
+ [.exportZipArchive],
+ [.viewLog]
+ ]
+
+ let tunnelsManager: TunnelsManager?
+ var wireguardCaptionedImage: (view: UIView, size: CGSize)?
+
+ init(tunnelsManager: TunnelsManager?) {
+ self.tunnelsManager = tunnelsManager
+ super.init(style: .grouped)
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ title = tr("settingsViewTitle")
+ navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneTapped))
+
+ tableView.estimatedRowHeight = 44
+ tableView.rowHeight = UITableView.automaticDimension
+ tableView.allowsSelection = false
+
+ tableView.register(KeyValueCell.self)
+ tableView.register(ButtonCell.self)
+
+ tableView.tableFooterView = UIImageView(image: UIImage(named: "wireguard.pdf"))
+ }
+
+ override func viewDidLayoutSubviews() {
+ super.viewDidLayoutSubviews()
+ guard let logo = tableView.tableFooterView else { return }
+
+ let bottomPadding = max(tableView.layoutMargins.bottom, 10)
+ let fullHeight = max(tableView.contentSize.height, tableView.bounds.size.height - tableView.layoutMargins.top - bottomPadding)
+
+ let imageAspectRatio = logo.intrinsicContentSize.width / logo.intrinsicContentSize.height
+
+ var height = tableView.estimatedRowHeight * 1.5
+ var width = height * imageAspectRatio
+ let maxWidth = view.bounds.size.width - max(tableView.layoutMargins.left + tableView.layoutMargins.right, 20)
+ if width > maxWidth {
+ width = maxWidth
+ height = width / imageAspectRatio
+ }
+
+ let needsReload = height != logo.frame.height
+
+ logo.frame = CGRect(x: (view.bounds.size.width - width) / 2, y: fullHeight - height, width: width, height: height)
+
+ if needsReload {
+ tableView.tableFooterView = logo
+ }
+ }
+
+ @objc func doneTapped() {
+ dismiss(animated: true, completion: nil)
+ }
+
+ func exportConfigurationsAsZipFile(sourceView: UIView) {
+ PrivateDataConfirmation.confirmAccess(to: tr("iosExportPrivateData")) { [weak self] in
+ guard let self = self else { return }
+ guard let tunnelsManager = self.tunnelsManager else { return }
+ guard let destinationDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return }
+
+ let destinationURL = destinationDir.appendingPathComponent("wireguard-export.zip")
+ _ = FileManager.deleteFile(at: destinationURL)
+
+ let count = tunnelsManager.numberOfTunnels()
+ let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration }
+ ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] error in
+ if let error = error {
+ ErrorPresenter.showErrorAlert(error: error, from: self)
+ return
+ }
+
+ let fileExportVC = UIDocumentPickerViewController(url: destinationURL, in: .exportToService)
+ self?.present(fileExportVC, animated: true, completion: nil)
+ }
+ }
+ }
+
+ func presentLogView() {
+ let logVC = LogViewController()
+ navigationController?.pushViewController(logVC, animated: true)
+
+ }
+}
+
+extension SettingsTableViewController {
+ override func numberOfSections(in tableView: UITableView) -> Int {
+ return settingsFieldsBySection.count
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return settingsFieldsBySection[section].count
+ }
+
+ override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ switch section {
+ case 0:
+ return tr("settingsSectionTitleAbout")
+ case 1:
+ return tr("settingsSectionTitleExportConfigurations")
+ case 2:
+ return tr("settingsSectionTitleTunnelLog")
+ default:
+ return nil
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let field = settingsFieldsBySection[indexPath.section][indexPath.row]
+ if field == .iosAppVersion || field == .goBackendVersion {
+ let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.copyableGesture = false
+ cell.key = field.localizedUIString
+ if field == .iosAppVersion {
+ var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown version"
+ if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
+ appVersion += " (\(appBuild))"
+ }
+ cell.value = appVersion
+ } else if field == .goBackendVersion {
+ cell.value = getWireGuardVersion()
+ }
+ return cell
+ } else if field == .exportZipArchive {
+ let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.buttonText = field.localizedUIString
+ cell.onTapped = { [weak self] in
+ self?.exportConfigurationsAsZipFile(sourceView: cell.button)
+ }
+ return cell
+ } else if field == .viewLog {
+ let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.buttonText = field.localizedUIString
+ cell.onTapped = { [weak self] in
+ self?.presentLogView()
+ }
+ return cell
+ } else if field == .donateLink {
+ let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.buttonText = field.localizedUIString
+ cell.onTapped = {
+ if let url = URL(string: "https://www.wireguard.com/donations/"), UIApplication.shared.canOpenURL(url) {
+ UIApplication.shared.open(url, options: [:])
+ }
+ }
+ return cell
+ }
+ fatalError()
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/ViewController/TunnelDetailTableViewController.swift b/Sources/WireGuardApp/UI/iOS/ViewController/TunnelDetailTableViewController.swift
new file mode 100644
index 0000000..d49acbc
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/ViewController/TunnelDetailTableViewController.swift
@@ -0,0 +1,458 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+import WireGuardKit
+
+class TunnelDetailTableViewController: UITableViewController {
+
+ private enum Section {
+ case status
+ case interface
+ case peer(index: Int, peer: TunnelViewModel.PeerData)
+ case onDemand
+ case delete
+ }
+
+ static let interfaceFields: [TunnelViewModel.InterfaceField] = [
+ .name, .publicKey, .addresses,
+ .listenPort, .mtu, .dns
+ ]
+
+ static let peerFields: [TunnelViewModel.PeerField] = [
+ .publicKey, .preSharedKey, .endpoint,
+ .allowedIPs, .persistentKeepAlive,
+ .rxBytes, .txBytes, .lastHandshakeTime
+ ]
+
+ static let onDemandFields: [ActivateOnDemandViewModel.OnDemandField] = [
+ .onDemand, .ssid
+ ]
+
+ let tunnelsManager: TunnelsManager
+ let tunnel: TunnelContainer
+ var tunnelViewModel: TunnelViewModel
+ var onDemandViewModel: ActivateOnDemandViewModel
+
+ private var sections = [Section]()
+ private var interfaceFieldIsVisible = [Bool]()
+ private var peerFieldIsVisible = [[Bool]]()
+
+ private var statusObservationToken: AnyObject?
+ private var onDemandObservationToken: AnyObject?
+ private var reloadRuntimeConfigurationTimer: Timer?
+
+ init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
+ self.tunnelsManager = tunnelsManager
+ self.tunnel = tunnel
+ tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
+ onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
+ super.init(style: .grouped)
+ loadSections()
+ loadVisibleFields()
+ statusObservationToken = tunnel.observe(\.status) { [weak self] _, _ in
+ guard let self = self else { return }
+ if tunnel.status == .active {
+ self.startUpdatingRuntimeConfiguration()
+ } else if tunnel.status == .inactive {
+ self.reloadRuntimeConfiguration()
+ self.stopUpdatingRuntimeConfiguration()
+ }
+ }
+ onDemandObservationToken = tunnel.observe(\.isActivateOnDemandEnabled) { [weak self] tunnel, _ in
+ // Handle On-Demand getting turned on/off outside of the app
+ self?.onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
+ self?.updateActivateOnDemandFields()
+ }
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ title = tunnelViewModel.interfaceData[.name]
+ navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
+
+ tableView.estimatedRowHeight = 44
+ tableView.rowHeight = UITableView.automaticDimension
+ tableView.register(SwitchCell.self)
+ tableView.register(KeyValueCell.self)
+ tableView.register(ButtonCell.self)
+ tableView.register(ChevronCell.self)
+
+ restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
+ }
+
+ private func loadSections() {
+ sections.removeAll()
+ sections.append(.status)
+ sections.append(.interface)
+ for (index, peer) in tunnelViewModel.peersData.enumerated() {
+ sections.append(.peer(index: index, peer: peer))
+ }
+ sections.append(.onDemand)
+ sections.append(.delete)
+ }
+
+ private func loadVisibleFields() {
+ let visibleInterfaceFields = tunnelViewModel.interfaceData.filterFieldsWithValueOrControl(interfaceFields: TunnelDetailTableViewController.interfaceFields)
+ interfaceFieldIsVisible = TunnelDetailTableViewController.interfaceFields.map { visibleInterfaceFields.contains($0) }
+ peerFieldIsVisible = tunnelViewModel.peersData.map { peer in
+ let visiblePeerFields = peer.filterFieldsWithValueOrControl(peerFields: TunnelDetailTableViewController.peerFields)
+ return TunnelDetailTableViewController.peerFields.map { visiblePeerFields.contains($0) }
+ }
+ }
+
+ override func viewWillAppear(_ animated: Bool) {
+ if tunnel.status == .active {
+ self.startUpdatingRuntimeConfiguration()
+ }
+ }
+
+ override func viewDidDisappear(_ animated: Bool) {
+ stopUpdatingRuntimeConfiguration()
+ }
+
+ @objc func editTapped() {
+ PrivateDataConfirmation.confirmAccess(to: tr("iosViewPrivateData")) { [weak self] in
+ guard let self = self else { return }
+ let editVC = TunnelEditTableViewController(tunnelsManager: self.tunnelsManager, tunnel: self.tunnel)
+ editVC.delegate = self
+ let editNC = UINavigationController(rootViewController: editVC)
+ editNC.modalPresentationStyle = .fullScreen
+ self.present(editNC, animated: true)
+ }
+ }
+
+ func startUpdatingRuntimeConfiguration() {
+ reloadRuntimeConfiguration()
+ reloadRuntimeConfigurationTimer?.invalidate()
+ let reloadTimer = Timer(timeInterval: 1 /* second */, repeats: true) { [weak self] _ in
+ self?.reloadRuntimeConfiguration()
+ }
+ reloadRuntimeConfigurationTimer = reloadTimer
+ RunLoop.main.add(reloadTimer, forMode: .common)
+ }
+
+ func stopUpdatingRuntimeConfiguration() {
+ reloadRuntimeConfigurationTimer?.invalidate()
+ reloadRuntimeConfigurationTimer = nil
+ }
+
+ func applyTunnelConfiguration(tunnelConfiguration: TunnelConfiguration) {
+ // Incorporates changes from tunnelConfiguation. Ignores any changes in peer ordering.
+ guard let tableView = self.tableView else { return }
+ let sections = self.sections
+ let interfaceSectionIndex = sections.firstIndex {
+ if case .interface = $0 {
+ return true
+ } else {
+ return false
+ }
+ }!
+ let firstPeerSectionIndex = interfaceSectionIndex + 1
+ var interfaceFieldIsVisible = self.interfaceFieldIsVisible
+ var peerFieldIsVisible = self.peerFieldIsVisible
+
+ func handleSectionFieldsModified<T>(fields: [T], fieldIsVisible: [Bool], section: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
+ for (index, field) in fields.enumerated() {
+ guard let change = changes[field] else { continue }
+ if case .modified(let newValue) = change {
+ let row = fieldIsVisible[0 ..< index].filter { $0 }.count
+ let indexPath = IndexPath(row: row, section: section)
+ if let cell = tableView.cellForRow(at: indexPath) as? KeyValueCell {
+ cell.value = newValue
+ }
+ }
+ }
+ }
+
+ func handleSectionRowsInsertedOrRemoved<T>(fields: [T], fieldIsVisible fieldIsVisibleInput: [Bool], section: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
+ var fieldIsVisible = fieldIsVisibleInput
+
+ var removedIndexPaths = [IndexPath]()
+ for (index, field) in fields.enumerated().reversed() where changes[field] == .removed {
+ let row = fieldIsVisible[0 ..< index].filter { $0 }.count
+ removedIndexPaths.append(IndexPath(row: row, section: section))
+ fieldIsVisible[index] = false
+ }
+ if !removedIndexPaths.isEmpty {
+ tableView.deleteRows(at: removedIndexPaths, with: .automatic)
+ }
+
+ var addedIndexPaths = [IndexPath]()
+ for (index, field) in fields.enumerated() where changes[field] == .added {
+ let row = fieldIsVisible[0 ..< index].filter { $0 }.count
+ addedIndexPaths.append(IndexPath(row: row, section: section))
+ fieldIsVisible[index] = true
+ }
+ if !addedIndexPaths.isEmpty {
+ tableView.insertRows(at: addedIndexPaths, with: .automatic)
+ }
+ }
+
+ let changes = self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration)
+
+ if !changes.interfaceChanges.isEmpty {
+ handleSectionFieldsModified(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible,
+ section: interfaceSectionIndex, changes: changes.interfaceChanges)
+ }
+ for (peerIndex, peerChanges) in changes.peerChanges {
+ handleSectionFieldsModified(fields: TunnelDetailTableViewController.peerFields, fieldIsVisible: peerFieldIsVisible[peerIndex], section: firstPeerSectionIndex + peerIndex, changes: peerChanges)
+ }
+
+ let isAnyInterfaceFieldAddedOrRemoved = changes.interfaceChanges.contains { $0.value == .added || $0.value == .removed }
+ let isAnyPeerFieldAddedOrRemoved = changes.peerChanges.contains { $0.changes.contains { $0.value == .added || $0.value == .removed } }
+ let peersRemovedSectionIndices = changes.peersRemovedIndices.map { firstPeerSectionIndex + $0 }
+ let peersInsertedSectionIndices = changes.peersInsertedIndices.map { firstPeerSectionIndex + $0 }
+
+ if isAnyInterfaceFieldAddedOrRemoved || isAnyPeerFieldAddedOrRemoved || !peersRemovedSectionIndices.isEmpty || !peersInsertedSectionIndices.isEmpty {
+ tableView.beginUpdates()
+ if isAnyInterfaceFieldAddedOrRemoved {
+ handleSectionRowsInsertedOrRemoved(fields: TunnelDetailTableViewController.interfaceFields, fieldIsVisible: interfaceFieldIsVisible, section: interfaceSectionIndex, changes: changes.interfaceChanges)
+ }
+ if isAnyPeerFieldAddedOrRemoved {
+ for (peerIndex, peerChanges) in changes.peerChanges {
+ handleSectionRowsInsertedOrRemoved(fields: TunnelDetailTableViewController.peerFields, fieldIsVisible: peerFieldIsVisible[peerIndex], section: firstPeerSectionIndex + peerIndex, changes: peerChanges)
+ }
+ }
+ if !peersRemovedSectionIndices.isEmpty {
+ tableView.deleteSections(IndexSet(peersRemovedSectionIndices), with: .automatic)
+ }
+ if !peersInsertedSectionIndices.isEmpty {
+ tableView.insertSections(IndexSet(peersInsertedSectionIndices), with: .automatic)
+ }
+ self.loadSections()
+ self.loadVisibleFields()
+ tableView.endUpdates()
+ } else {
+ self.loadSections()
+ self.loadVisibleFields()
+ }
+ }
+
+ private func reloadRuntimeConfiguration() {
+ tunnel.getRuntimeTunnelConfiguration { [weak self] tunnelConfiguration in
+ guard let tunnelConfiguration = tunnelConfiguration else { return }
+ guard let self = self else { return }
+ self.applyTunnelConfiguration(tunnelConfiguration: tunnelConfiguration)
+ }
+ }
+
+ private func updateActivateOnDemandFields() {
+ guard let onDemandSection = sections.firstIndex(where: { if case .onDemand = $0 { return true } else { return false } }) else { return }
+ let numberOfTableViewOnDemandRows = tableView.numberOfRows(inSection: onDemandSection)
+ let ssidRowIndexPath = IndexPath(row: 1, section: onDemandSection)
+ switch (numberOfTableViewOnDemandRows, onDemandViewModel.isWiFiInterfaceEnabled) {
+ case (1, true):
+ tableView.insertRows(at: [ssidRowIndexPath], with: .automatic)
+ case (2, false):
+ tableView.deleteRows(at: [ssidRowIndexPath], with: .automatic)
+ default:
+ break
+ }
+ tableView.reloadSections(IndexSet(integer: onDemandSection), with: .automatic)
+ }
+}
+
+extension TunnelDetailTableViewController: TunnelEditTableViewControllerDelegate {
+ func tunnelSaved(tunnel: TunnelContainer) {
+ tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
+ onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
+ loadSections()
+ loadVisibleFields()
+ title = tunnel.name
+ restorationIdentifier = "TunnelDetailVC:\(tunnel.name)"
+ tableView.reloadData()
+ }
+ func tunnelEditingCancelled() {
+ // Nothing to do
+ }
+}
+
+extension TunnelDetailTableViewController {
+ override func numberOfSections(in tableView: UITableView) -> Int {
+ return sections.count
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch sections[section] {
+ case .status:
+ return 1
+ case .interface:
+ return interfaceFieldIsVisible.filter { $0 }.count
+ case .peer(let peerIndex, _):
+ return peerFieldIsVisible[peerIndex].filter { $0 }.count
+ case .onDemand:
+ return onDemandViewModel.isWiFiInterfaceEnabled ? 2 : 1
+ case .delete:
+ return 1
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ switch sections[section] {
+ case .status:
+ return tr("tunnelSectionTitleStatus")
+ case .interface:
+ return tr("tunnelSectionTitleInterface")
+ case .peer:
+ return tr("tunnelSectionTitlePeer")
+ case .onDemand:
+ return tr("tunnelSectionTitleOnDemand")
+ case .delete:
+ return nil
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ switch sections[indexPath.section] {
+ case .status:
+ return statusCell(for: tableView, at: indexPath)
+ case .interface:
+ return interfaceCell(for: tableView, at: indexPath)
+ case .peer(let index, let peer):
+ return peerCell(for: tableView, at: indexPath, with: peer, peerIndex: index)
+ case .onDemand:
+ return onDemandCell(for: tableView, at: indexPath)
+ case .delete:
+ return deleteConfigurationCell(for: tableView, at: indexPath)
+ }
+ }
+
+ private func statusCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
+
+ let statusUpdate: (SwitchCell, TunnelStatus) -> Void = { cell, status in
+ let text: String
+ switch status {
+ case .inactive:
+ text = tr("tunnelStatusInactive")
+ case .activating:
+ text = tr("tunnelStatusActivating")
+ case .active:
+ text = tr("tunnelStatusActive")
+ case .deactivating:
+ text = tr("tunnelStatusDeactivating")
+ case .reasserting:
+ text = tr("tunnelStatusReasserting")
+ case .restarting:
+ text = tr("tunnelStatusRestarting")
+ case .waiting:
+ text = tr("tunnelStatusWaiting")
+ }
+ cell.textLabel?.text = text
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak cell] in
+ cell?.switchView.isOn = !(status == .deactivating || status == .inactive)
+ cell?.switchView.isUserInteractionEnabled = (status == .inactive || status == .active)
+ }
+ cell.isEnabled = status == .active || status == .inactive
+ }
+
+ statusUpdate(cell, tunnel.status)
+ cell.observationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
+ guard let cell = cell else { return }
+ statusUpdate(cell, tunnel.status)
+ }
+
+ cell.onSwitchToggled = { [weak self] isOn in
+ guard let self = self else { return }
+ if isOn {
+ self.tunnelsManager.startActivation(of: self.tunnel)
+ } else {
+ self.tunnelsManager.startDeactivation(of: self.tunnel)
+ }
+ }
+ return cell
+ }
+
+ private func interfaceCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ let visibleInterfaceFields = TunnelDetailTableViewController.interfaceFields.enumerated().filter { interfaceFieldIsVisible[$0.offset] }.map { $0.element }
+ let field = visibleInterfaceFields[indexPath.row]
+ let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.key = field.localizedUIString
+ cell.value = tunnelViewModel.interfaceData[field]
+ return cell
+ }
+
+ private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData, peerIndex: Int) -> UITableViewCell {
+ let visiblePeerFields = TunnelDetailTableViewController.peerFields.enumerated().filter { peerFieldIsVisible[peerIndex][$0.offset] }.map { $0.element }
+ let field = visiblePeerFields[indexPath.row]
+ let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.key = field.localizedUIString
+ if field == .persistentKeepAlive {
+ cell.value = tr(format: "tunnelPeerPersistentKeepaliveValue (%@)", peerData[field])
+ } else if field == .preSharedKey {
+ cell.value = tr("tunnelPeerPresharedKeyEnabled")
+ } else {
+ cell.value = peerData[field]
+ }
+ return cell
+ }
+
+ private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ let field = TunnelDetailTableViewController.onDemandFields[indexPath.row]
+ if field == .onDemand {
+ let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.key = field.localizedUIString
+ cell.value = onDemandViewModel.localizedInterfaceDescription
+ return cell
+ } else {
+ assert(field == .ssid)
+ if onDemandViewModel.ssidOption == .anySSID {
+ let cell: KeyValueCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.key = field.localizedUIString
+ cell.value = onDemandViewModel.ssidOption.localizedUIString
+ return cell
+ } else {
+ let cell: ChevronCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.message = field.localizedUIString
+ cell.detailMessage = onDemandViewModel.localizedSSIDDescription
+ return cell
+ }
+ }
+ }
+
+ private func deleteConfigurationCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.buttonText = tr("deleteTunnelButtonTitle")
+ cell.hasDestructiveAction = true
+ cell.onTapped = { [weak self] in
+ guard let self = self else { return }
+ ConfirmationAlertPresenter.showConfirmationAlert(message: tr("deleteTunnelConfirmationAlertMessage"),
+ buttonTitle: tr("deleteTunnelConfirmationAlertButtonTitle"),
+ from: cell, presentingVC: self) { [weak self] in
+ guard let self = self else { return }
+ self.tunnelsManager.remove(tunnel: self.tunnel) { error in
+ if error != nil {
+ print("Error removing tunnel: \(String(describing: error))")
+ return
+ }
+ }
+ }
+ }
+ return cell
+ }
+
+}
+
+extension TunnelDetailTableViewController {
+ override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
+ if case .onDemand = sections[indexPath.section],
+ case .ssid = TunnelDetailTableViewController.onDemandFields[indexPath.row] {
+ return indexPath
+ }
+ return nil
+ }
+
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ if case .onDemand = sections[indexPath.section],
+ case .ssid = TunnelDetailTableViewController.onDemandFields[indexPath.row] {
+ let ssidDetailVC = SSIDOptionDetailTableViewController(title: onDemandViewModel.ssidOption.localizedUIString, ssids: onDemandViewModel.selectedSSIDs)
+ navigationController?.pushViewController(ssidDetailVC, animated: true)
+ }
+ tableView.deselectRow(at: indexPath, animated: true)
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/ViewController/TunnelEditTableViewController.swift b/Sources/WireGuardApp/UI/iOS/ViewController/TunnelEditTableViewController.swift
new file mode 100644
index 0000000..ecad2f6
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/ViewController/TunnelEditTableViewController.swift
@@ -0,0 +1,504 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+import WireGuardKit
+
+protocol TunnelEditTableViewControllerDelegate: class {
+ func tunnelSaved(tunnel: TunnelContainer)
+ func tunnelEditingCancelled()
+}
+
+class TunnelEditTableViewController: UITableViewController {
+ private enum Section {
+ case interface
+ case peer(_ peer: TunnelViewModel.PeerData)
+ case addPeer
+ case onDemand
+
+ static func == (lhs: Section, rhs: Section) -> Bool {
+ switch (lhs, rhs) {
+ case (.interface, .interface),
+ (.addPeer, .addPeer),
+ (.onDemand, .onDemand):
+ return true
+ case let (.peer(peerA), .peer(peerB)):
+ return peerA.index == peerB.index
+ default:
+ return false
+ }
+ }
+ }
+
+ weak var delegate: TunnelEditTableViewControllerDelegate?
+
+ let interfaceFieldsBySection: [[TunnelViewModel.InterfaceField]] = [
+ [.name],
+ [.privateKey, .publicKey, .generateKeyPair],
+ [.addresses, .listenPort, .mtu, .dns]
+ ]
+
+ let peerFields: [TunnelViewModel.PeerField] = [
+ .publicKey, .preSharedKey, .endpoint,
+ .allowedIPs, .excludePrivateIPs, .persistentKeepAlive,
+ .deletePeer
+ ]
+
+ let onDemandFields: [ActivateOnDemandViewModel.OnDemandField] = [
+ .nonWiFiInterface,
+ .wiFiInterface,
+ .ssid
+ ]
+
+ let tunnelsManager: TunnelsManager
+ let tunnel: TunnelContainer?
+ let tunnelViewModel: TunnelViewModel
+ var onDemandViewModel: ActivateOnDemandViewModel
+ private var sections = [Section]()
+
+ // Use this initializer to edit an existing tunnel.
+ init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
+ self.tunnelsManager = tunnelsManager
+ self.tunnel = tunnel
+ tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
+ onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
+ super.init(style: .grouped)
+ loadSections()
+ }
+
+ // Use this initializer to create a new tunnel.
+ init(tunnelsManager: TunnelsManager) {
+ self.tunnelsManager = tunnelsManager
+ tunnel = nil
+ tunnelViewModel = TunnelViewModel(tunnelConfiguration: nil)
+ onDemandViewModel = ActivateOnDemandViewModel()
+ super.init(style: .grouped)
+ loadSections()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+ title = tunnel == nil ? tr("newTunnelViewTitle") : tr("editTunnelViewTitle")
+ navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(saveTapped))
+ navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelTapped))
+
+ tableView.estimatedRowHeight = 44
+ tableView.rowHeight = UITableView.automaticDimension
+
+ tableView.register(TunnelEditKeyValueCell.self)
+ tableView.register(TunnelEditEditableKeyValueCell.self)
+ tableView.register(ButtonCell.self)
+ tableView.register(SwitchCell.self)
+ tableView.register(ChevronCell.self)
+ }
+
+ private func loadSections() {
+ sections.removeAll()
+ interfaceFieldsBySection.forEach { _ in sections.append(.interface) }
+ tunnelViewModel.peersData.forEach { sections.append(.peer($0)) }
+ sections.append(.addPeer)
+ sections.append(.onDemand)
+ }
+
+ @objc func saveTapped() {
+ tableView.endEditing(false)
+ let tunnelSaveResult = tunnelViewModel.save()
+ switch tunnelSaveResult {
+ case .error(let errorMessage):
+ let alertTitle = (tunnelViewModel.interfaceData.validatedConfiguration == nil || tunnelViewModel.interfaceData.validatedName == nil) ?
+ tr("alertInvalidInterfaceTitle") : tr("alertInvalidPeerTitle")
+ ErrorPresenter.showErrorAlert(title: alertTitle, message: errorMessage, from: self)
+ tableView.reloadData() // Highlight erroring fields
+ case .saved(let tunnelConfiguration):
+ let onDemandOption = onDemandViewModel.toOnDemandOption()
+ if let tunnel = tunnel {
+ // We're modifying an existing tunnel
+ tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, onDemandOption: onDemandOption) { [weak self] error in
+ if let error = error {
+ ErrorPresenter.showErrorAlert(error: error, from: self)
+ } else {
+ self?.dismiss(animated: true, completion: nil)
+ self?.delegate?.tunnelSaved(tunnel: tunnel)
+ }
+ }
+ } else {
+ // We're adding a new tunnel
+ tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, onDemandOption: onDemandOption) { [weak self] result in
+ switch result {
+ case .failure(let error):
+ ErrorPresenter.showErrorAlert(error: error, from: self)
+ case .success(let tunnel):
+ self?.dismiss(animated: true, completion: nil)
+ self?.delegate?.tunnelSaved(tunnel: tunnel)
+ }
+ }
+ }
+ }
+ }
+
+ @objc func cancelTapped() {
+ dismiss(animated: true, completion: nil)
+ delegate?.tunnelEditingCancelled()
+ }
+}
+
+// MARK: UITableViewDataSource
+
+extension TunnelEditTableViewController {
+ override func numberOfSections(in tableView: UITableView) -> Int {
+ return sections.count
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ switch sections[section] {
+ case .interface:
+ return interfaceFieldsBySection[section].count
+ case .peer(let peerData):
+ let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
+ return peerFieldsToShow.count
+ case .addPeer:
+ return 1
+ case .onDemand:
+ if onDemandViewModel.isWiFiInterfaceEnabled {
+ return 3
+ } else {
+ return 2
+ }
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+ switch sections[section] {
+ case .interface:
+ return section == 0 ? tr("tunnelSectionTitleInterface") : nil
+ case .peer:
+ return tr("tunnelSectionTitlePeer")
+ case .addPeer:
+ return nil
+ case .onDemand:
+ return tr("tunnelSectionTitleOnDemand")
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ switch sections[indexPath.section] {
+ case .interface:
+ return interfaceFieldCell(for: tableView, at: indexPath)
+ case .peer(let peerData):
+ return peerCell(for: tableView, at: indexPath, with: peerData)
+ case .addPeer:
+ return addPeerCell(for: tableView, at: indexPath)
+ case .onDemand:
+ return onDemandCell(for: tableView, at: indexPath)
+ }
+ }
+
+ private func interfaceFieldCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ let field = interfaceFieldsBySection[indexPath.section][indexPath.row]
+ switch field {
+ case .generateKeyPair:
+ return generateKeyPairCell(for: tableView, at: indexPath, with: field)
+ case .publicKey:
+ return publicKeyCell(for: tableView, at: indexPath, with: field)
+ default:
+ return interfaceFieldKeyValueCell(for: tableView, at: indexPath, with: field)
+ }
+ }
+
+ private func generateKeyPairCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
+ let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.buttonText = field.localizedUIString
+ cell.onTapped = { [weak self] in
+ guard let self = self else { return }
+
+ self.tunnelViewModel.interfaceData[.privateKey] = PrivateKey().base64Key
+ if let privateKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .privateKey),
+ let publicKeyRow = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) {
+ let privateKeyIndex = IndexPath(row: privateKeyRow, section: indexPath.section)
+ let publicKeyIndex = IndexPath(row: publicKeyRow, section: indexPath.section)
+ self.tableView.reloadRows(at: [privateKeyIndex, publicKeyIndex], with: .fade)
+ }
+ }
+ return cell
+ }
+
+ private func publicKeyCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
+ let cell: TunnelEditKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.key = field.localizedUIString
+ cell.value = tunnelViewModel.interfaceData[field]
+ return cell
+ }
+
+ private func interfaceFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, with field: TunnelViewModel.InterfaceField) -> UITableViewCell {
+ let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.key = field.localizedUIString
+
+ switch field {
+ case .name, .privateKey:
+ cell.placeholderText = tr("tunnelEditPlaceholderTextRequired")
+ cell.keyboardType = .default
+ case .addresses:
+ cell.placeholderText = tr("tunnelEditPlaceholderTextStronglyRecommended")
+ cell.keyboardType = .numbersAndPunctuation
+ case .dns:
+ cell.placeholderText = tunnelViewModel.peersData.contains(where: { $0.shouldStronglyRecommendDNS }) ? tr("tunnelEditPlaceholderTextStronglyRecommended") : tr("tunnelEditPlaceholderTextOptional")
+ cell.keyboardType = .numbersAndPunctuation
+ case .listenPort, .mtu:
+ cell.placeholderText = tr("tunnelEditPlaceholderTextAutomatic")
+ cell.keyboardType = .numberPad
+ case .publicKey, .generateKeyPair:
+ cell.keyboardType = .default
+ case .status, .toggleStatus:
+ fatalError("Unexpected interface field")
+ }
+
+ cell.isValueValid = (!tunnelViewModel.interfaceData.fieldsWithError.contains(field))
+ // Bind values to view model
+ cell.value = tunnelViewModel.interfaceData[field]
+ if field == .dns { // While editing DNS, you might directly set exclude private IPs
+ cell.onValueBeingEdited = { [weak self] value in
+ self?.tunnelViewModel.interfaceData[field] = value
+ }
+ cell.onValueChanged = { [weak self] oldValue, newValue in
+ guard let self = self else { return }
+ let isAllowedIPsChanged = self.tunnelViewModel.updateDNSServersInAllowedIPsIfRequired(oldDNSServers: oldValue, newDNSServers: newValue)
+ if isAllowedIPsChanged {
+ let section = self.sections.firstIndex { if case .peer(_) = $0 { return true } else { return false } }
+ if let section = section, let row = self.peerFields.firstIndex(of: .allowedIPs) {
+ self.tableView.reloadRows(at: [IndexPath(row: row, section: section)], with: .none)
+ }
+ }
+ }
+ } else {
+ cell.onValueChanged = { [weak self] _, value in
+ self?.tunnelViewModel.interfaceData[field] = value
+ }
+ }
+ // Compute public key live
+ if field == .privateKey {
+ cell.onValueBeingEdited = { [weak self] value in
+ guard let self = self else { return }
+
+ self.tunnelViewModel.interfaceData[.privateKey] = value
+ if let row = self.interfaceFieldsBySection[indexPath.section].firstIndex(of: .publicKey) {
+ self.tableView.reloadRows(at: [IndexPath(row: row, section: indexPath.section)], with: .none)
+ }
+ }
+ }
+ return cell
+ }
+
+ private func peerCell(for tableView: UITableView, at indexPath: IndexPath, with peerData: TunnelViewModel.PeerData) -> UITableViewCell {
+ let peerFieldsToShow = peerData.shouldAllowExcludePrivateIPsControl ? peerFields : peerFields.filter { $0 != .excludePrivateIPs }
+ let field = peerFieldsToShow[indexPath.row]
+
+ switch field {
+ case .deletePeer:
+ return deletePeerCell(for: tableView, at: indexPath, peerData: peerData, field: field)
+ case .excludePrivateIPs:
+ return excludePrivateIPsCell(for: tableView, at: indexPath, peerData: peerData, field: field)
+ default:
+ return peerFieldKeyValueCell(for: tableView, at: indexPath, peerData: peerData, field: field)
+ }
+ }
+
+ private func deletePeerCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
+ let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.buttonText = field.localizedUIString
+ cell.hasDestructiveAction = true
+ cell.onTapped = { [weak self, weak peerData] in
+ guard let self = self, let peerData = peerData else { return }
+ ConfirmationAlertPresenter.showConfirmationAlert(message: tr("deletePeerConfirmationAlertMessage"),
+ buttonTitle: tr("deletePeerConfirmationAlertButtonTitle"),
+ from: cell, presentingVC: self) { [weak self] in
+ guard let self = self else { return }
+ let removedSectionIndices = self.deletePeer(peer: peerData)
+ let shouldShowExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
+
+ //swiftlint:disable:next trailing_closure
+ tableView.performBatchUpdates({
+ self.tableView.deleteSections(removedSectionIndices, with: .fade)
+ if shouldShowExcludePrivateIPs {
+ if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) {
+ let rowIndexPath = IndexPath(row: row, section: self.interfaceFieldsBySection.count /* First peer section */)
+ self.tableView.insertRows(at: [rowIndexPath], with: .fade)
+ }
+ }
+ })
+ }
+ }
+ return cell
+ }
+
+ private func excludePrivateIPsCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
+ let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.message = field.localizedUIString
+ cell.isEnabled = peerData.shouldAllowExcludePrivateIPsControl
+ cell.isOn = peerData.excludePrivateIPsValue
+ cell.onSwitchToggled = { [weak self] isOn in
+ guard let self = self else { return }
+ peerData.excludePrivateIPsValueChanged(isOn: isOn, dnsServers: self.tunnelViewModel.interfaceData[.dns])
+ if let row = self.peerFields.firstIndex(of: .allowedIPs) {
+ self.tableView.reloadRows(at: [IndexPath(row: row, section: indexPath.section)], with: .none)
+ }
+ }
+ return cell
+ }
+
+ private func peerFieldKeyValueCell(for tableView: UITableView, at indexPath: IndexPath, peerData: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField) -> UITableViewCell {
+ let cell: TunnelEditEditableKeyValueCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.key = field.localizedUIString
+
+ switch field {
+ case .publicKey:
+ cell.placeholderText = tr("tunnelEditPlaceholderTextRequired")
+ cell.keyboardType = .default
+ case .preSharedKey, .endpoint:
+ cell.placeholderText = tr("tunnelEditPlaceholderTextOptional")
+ cell.keyboardType = .default
+ case .allowedIPs:
+ cell.placeholderText = tr("tunnelEditPlaceholderTextOptional")
+ cell.keyboardType = .numbersAndPunctuation
+ case .persistentKeepAlive:
+ cell.placeholderText = tr("tunnelEditPlaceholderTextOff")
+ cell.keyboardType = .numberPad
+ case .excludePrivateIPs, .deletePeer:
+ cell.keyboardType = .default
+ case .rxBytes, .txBytes, .lastHandshakeTime:
+ fatalError()
+ }
+
+ cell.isValueValid = !peerData.fieldsWithError.contains(field)
+ cell.value = peerData[field]
+
+ if field == .allowedIPs {
+ let firstInterfaceSection = sections.firstIndex { $0 == .interface }!
+ let interfaceSubSection = interfaceFieldsBySection.firstIndex { $0.contains(.dns) }!
+ let dnsRow = interfaceFieldsBySection[interfaceSubSection].firstIndex { $0 == .dns }!
+
+ cell.onValueBeingEdited = { [weak self, weak peerData] value in
+ guard let self = self, let peerData = peerData else { return }
+
+ let oldValue = peerData.shouldAllowExcludePrivateIPsControl
+ peerData[.allowedIPs] = value
+ if oldValue != peerData.shouldAllowExcludePrivateIPsControl, let row = self.peerFields.firstIndex(of: .excludePrivateIPs) {
+ if peerData.shouldAllowExcludePrivateIPsControl {
+ self.tableView.insertRows(at: [IndexPath(row: row, section: indexPath.section)], with: .fade)
+ } else {
+ self.tableView.deleteRows(at: [IndexPath(row: row, section: indexPath.section)], with: .fade)
+ }
+ }
+
+ tableView.reloadRows(at: [IndexPath(row: dnsRow, section: firstInterfaceSection + interfaceSubSection)], with: .none)
+ }
+ } else {
+ cell.onValueChanged = { [weak peerData] _, value in
+ peerData?[field] = value
+ }
+ }
+
+ return cell
+ }
+
+ private func addPeerCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ let cell: ButtonCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.buttonText = tr("addPeerButtonTitle")
+ cell.onTapped = { [weak self] in
+ guard let self = self else { return }
+ let shouldHideExcludePrivateIPs = (self.tunnelViewModel.peersData.count == 1 && self.tunnelViewModel.peersData[0].shouldAllowExcludePrivateIPsControl)
+ let addedSectionIndices = self.appendEmptyPeer()
+ tableView.performBatchUpdates({
+ tableView.insertSections(addedSectionIndices, with: .fade)
+ if shouldHideExcludePrivateIPs {
+ if let row = self.peerFields.firstIndex(of: .excludePrivateIPs) {
+ let rowIndexPath = IndexPath(row: row, section: self.interfaceFieldsBySection.count /* First peer section */)
+ self.tableView.deleteRows(at: [rowIndexPath], with: .fade)
+ }
+ }
+ }, completion: nil)
+ }
+ return cell
+ }
+
+ private func onDemandCell(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell {
+ let field = onDemandFields[indexPath.row]
+ if indexPath.row < 2 {
+ let cell: SwitchCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.message = field.localizedUIString
+ cell.isOn = onDemandViewModel.isEnabled(field: field)
+ cell.onSwitchToggled = { [weak self] isOn in
+ guard let self = self else { return }
+ self.onDemandViewModel.setEnabled(field: field, isEnabled: isOn)
+ let section = self.sections.firstIndex { $0 == .onDemand }!
+ let indexPath = IndexPath(row: 2, section: section)
+ if field == .wiFiInterface {
+ if isOn {
+ tableView.insertRows(at: [indexPath], with: .fade)
+ } else {
+ tableView.deleteRows(at: [indexPath], with: .fade)
+ }
+ }
+ }
+ return cell
+ } else {
+ let cell: ChevronCell = tableView.dequeueReusableCell(for: indexPath)
+ cell.message = field.localizedUIString
+ cell.detailMessage = onDemandViewModel.localizedSSIDDescription
+ return cell
+ }
+ }
+
+ func appendEmptyPeer() -> IndexSet {
+ tunnelViewModel.appendEmptyPeer()
+ loadSections()
+ let addedPeerIndex = tunnelViewModel.peersData.count - 1
+ return IndexSet(integer: interfaceFieldsBySection.count + addedPeerIndex)
+ }
+
+ func deletePeer(peer: TunnelViewModel.PeerData) -> IndexSet {
+ tunnelViewModel.deletePeer(peer: peer)
+ loadSections()
+ return IndexSet(integer: interfaceFieldsBySection.count + peer.index)
+ }
+}
+
+extension TunnelEditTableViewController {
+ override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
+ if case .onDemand = sections[indexPath.section], indexPath.row == 2 {
+ return indexPath
+ } else {
+ return nil
+ }
+ }
+
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ switch sections[indexPath.section] {
+ case .onDemand:
+ assert(indexPath.row == 2)
+ tableView.deselectRow(at: indexPath, animated: true)
+ let ssidOptionVC = SSIDOptionEditTableViewController(option: onDemandViewModel.ssidOption, ssids: onDemandViewModel.selectedSSIDs)
+ ssidOptionVC.delegate = self
+ navigationController?.pushViewController(ssidOptionVC, animated: true)
+ default:
+ assertionFailure()
+ }
+ }
+}
+
+extension TunnelEditTableViewController: SSIDOptionEditTableViewControllerDelegate {
+ func ssidOptionSaved(option: ActivateOnDemandViewModel.OnDemandSSIDOption, ssids: [String]) {
+ onDemandViewModel.selectedSSIDs = ssids
+ onDemandViewModel.ssidOption = option
+ onDemandViewModel.fixSSIDOption()
+ if let onDemandSection = sections.firstIndex(where: { $0 == .onDemand }) {
+ if let ssidRowIndex = onDemandFields.firstIndex(of: .ssid) {
+ let indexPath = IndexPath(row: ssidRowIndex, section: onDemandSection)
+ tableView.reloadRows(at: [indexPath], with: .none)
+ }
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/ViewController/TunnelsListTableViewController.swift b/Sources/WireGuardApp/UI/iOS/ViewController/TunnelsListTableViewController.swift
new file mode 100644
index 0000000..f442420
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/ViewController/TunnelsListTableViewController.swift
@@ -0,0 +1,429 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import UIKit
+import MobileCoreServices
+import UserNotifications
+import WireGuardKit
+
+class TunnelsListTableViewController: UIViewController {
+
+ var tunnelsManager: TunnelsManager?
+
+ enum TableState: Equatable {
+ case normal
+ case rowSwiped
+ case multiSelect(selectionCount: Int)
+ }
+
+ let tableView: UITableView = {
+ let tableView = UITableView(frame: CGRect.zero, style: .plain)
+ tableView.estimatedRowHeight = 60
+ tableView.rowHeight = UITableView.automaticDimension
+ tableView.separatorStyle = .none
+ tableView.register(TunnelListCell.self)
+ return tableView
+ }()
+
+ let centeredAddButton: BorderedTextButton = {
+ let button = BorderedTextButton()
+ button.title = tr("tunnelsListCenteredAddTunnelButtonTitle")
+ button.isHidden = true
+ return button
+ }()
+
+ let busyIndicator: UIActivityIndicatorView = {
+ if #available(iOS 13.0, *) {
+ let busyIndicator = UIActivityIndicatorView(style: .medium)
+ busyIndicator.hidesWhenStopped = true
+ return busyIndicator
+ } else {
+ let busyIndicator = UIActivityIndicatorView(style: .gray)
+ busyIndicator.hidesWhenStopped = true
+ return busyIndicator
+ }
+ }()
+
+ var detailDisplayedTunnel: TunnelContainer?
+ var tableState: TableState = .normal {
+ didSet {
+ handleTableStateChange()
+ }
+ }
+
+ override func loadView() {
+ view = UIView()
+ if #available(iOS 13.0, *) {
+ view.backgroundColor = .systemBackground
+ } else {
+ view.backgroundColor = .white
+ }
+
+ tableView.dataSource = self
+ tableView.delegate = self
+
+ view.addSubview(tableView)
+ tableView.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ tableView.topAnchor.constraint(equalTo: view.topAnchor),
+ tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
+ ])
+
+ view.addSubview(busyIndicator)
+ busyIndicator.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ busyIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ busyIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
+ ])
+
+ view.addSubview(centeredAddButton)
+ centeredAddButton.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ centeredAddButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ centeredAddButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
+ ])
+
+ centeredAddButton.onTapped = { [weak self] in
+ guard let self = self else { return }
+ self.addButtonTapped(sender: self.centeredAddButton)
+ }
+
+ busyIndicator.startAnimating()
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ tableState = .normal
+ restorationIdentifier = "TunnelsListVC"
+ }
+
+ func handleTableStateChange() {
+ switch tableState {
+ case .normal:
+ navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addButtonTapped(sender:)))
+ navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSettingsButtonTitle"), style: .plain, target: self, action: #selector(settingsButtonTapped(sender:)))
+ case .rowSwiped:
+ navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTapped))
+ navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSelectButtonTitle"), style: .plain, target: self, action: #selector(selectButtonTapped))
+ case .multiSelect(let selectionCount):
+ if selectionCount > 0 {
+ navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonTapped))
+ navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListDeleteButtonTitle"), style: .plain, target: self, action: #selector(deleteButtonTapped(sender:)))
+ } else {
+ navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancelButtonTapped))
+ navigationItem.leftBarButtonItem = UIBarButtonItem(title: tr("tunnelsListSelectAllButtonTitle"), style: .plain, target: self, action: #selector(selectAllButtonTapped))
+ }
+ }
+ if case .multiSelect(let selectionCount) = tableState, selectionCount > 0 {
+ navigationItem.title = tr(format: "tunnelsListSelectedTitle (%d)", selectionCount)
+ } else {
+ navigationItem.title = tr("tunnelsListTitle")
+ }
+ if case .multiSelect = tableState {
+ tableView.allowsMultipleSelectionDuringEditing = true
+ } else {
+ tableView.allowsMultipleSelectionDuringEditing = false
+ }
+ }
+
+ func setTunnelsManager(tunnelsManager: TunnelsManager) {
+ self.tunnelsManager = tunnelsManager
+ tunnelsManager.tunnelsListDelegate = self
+
+ busyIndicator.stopAnimating()
+ tableView.reloadData()
+ centeredAddButton.isHidden = tunnelsManager.numberOfTunnels() > 0
+ }
+
+ override func viewWillAppear(_: Bool) {
+ if let selectedRowIndexPath = tableView.indexPathForSelectedRow {
+ tableView.deselectRow(at: selectedRowIndexPath, animated: false)
+ }
+ }
+
+ @objc func addButtonTapped(sender: AnyObject) {
+ guard tunnelsManager != nil else { return }
+
+ let alert = UIAlertController(title: "", message: tr("addTunnelMenuHeader"), preferredStyle: .actionSheet)
+ let importFileAction = UIAlertAction(title: tr("addTunnelMenuImportFile"), style: .default) { [weak self] _ in
+ self?.presentViewControllerForFileImport()
+ }
+ alert.addAction(importFileAction)
+
+ let scanQRCodeAction = UIAlertAction(title: tr("addTunnelMenuQRCode"), style: .default) { [weak self] _ in
+ self?.presentViewControllerForScanningQRCode()
+ }
+ alert.addAction(scanQRCodeAction)
+
+ let createFromScratchAction = UIAlertAction(title: tr("addTunnelMenuFromScratch"), style: .default) { [weak self] _ in
+ if let self = self, let tunnelsManager = self.tunnelsManager {
+ self.presentViewControllerForTunnelCreation(tunnelsManager: tunnelsManager)
+ }
+ }
+ alert.addAction(createFromScratchAction)
+
+ let cancelAction = UIAlertAction(title: tr("actionCancel"), style: .cancel)
+ alert.addAction(cancelAction)
+
+ if let sender = sender as? UIBarButtonItem {
+ alert.popoverPresentationController?.barButtonItem = sender
+ } else if let sender = sender as? UIView {
+ alert.popoverPresentationController?.sourceView = sender
+ alert.popoverPresentationController?.sourceRect = sender.bounds
+ }
+ present(alert, animated: true, completion: nil)
+ }
+
+ @objc func settingsButtonTapped(sender: UIBarButtonItem) {
+ guard tunnelsManager != nil else { return }
+
+ let settingsVC = SettingsTableViewController(tunnelsManager: tunnelsManager)
+ let settingsNC = UINavigationController(rootViewController: settingsVC)
+ settingsNC.modalPresentationStyle = .formSheet
+ present(settingsNC, animated: true)
+ }
+
+ func presentViewControllerForTunnelCreation(tunnelsManager: TunnelsManager) {
+ let editVC = TunnelEditTableViewController(tunnelsManager: tunnelsManager)
+ let editNC = UINavigationController(rootViewController: editVC)
+ editNC.modalPresentationStyle = .fullScreen
+ present(editNC, animated: true)
+ }
+
+ func presentViewControllerForFileImport() {
+ let documentTypes = ["com.wireguard.config.quick", String(kUTTypeText), String(kUTTypeZipArchive)]
+ let filePicker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
+ filePicker.delegate = self
+ present(filePicker, animated: true)
+ }
+
+ func presentViewControllerForScanningQRCode() {
+ let scanQRCodeVC = QRScanViewController()
+ scanQRCodeVC.delegate = self
+ let scanQRCodeNC = UINavigationController(rootViewController: scanQRCodeVC)
+ scanQRCodeNC.modalPresentationStyle = .fullScreen
+ present(scanQRCodeNC, animated: true)
+ }
+
+ @objc func selectButtonTapped() {
+ let shouldCancelSwipe = tableState == .rowSwiped
+ tableState = .multiSelect(selectionCount: 0)
+ if shouldCancelSwipe {
+ tableView.setEditing(false, animated: false)
+ }
+ tableView.setEditing(true, animated: true)
+ }
+
+ @objc func doneButtonTapped() {
+ tableState = .normal
+ tableView.setEditing(false, animated: true)
+ }
+
+ @objc func selectAllButtonTapped() {
+ guard tableView.isEditing else { return }
+ guard let tunnelsManager = tunnelsManager else { return }
+ for index in 0 ..< tunnelsManager.numberOfTunnels() {
+ tableView.selectRow(at: IndexPath(row: index, section: 0), animated: false, scrollPosition: .none)
+ }
+ tableState = .multiSelect(selectionCount: tableView.indexPathsForSelectedRows?.count ?? 0)
+ }
+
+ @objc func cancelButtonTapped() {
+ tableState = .normal
+ tableView.setEditing(false, animated: true)
+ }
+
+ @objc func deleteButtonTapped(sender: AnyObject?) {
+ guard let sender = sender as? UIBarButtonItem else { return }
+ guard let tunnelsManager = tunnelsManager else { return }
+
+ let selectedTunnelIndices = tableView.indexPathsForSelectedRows?.map { $0.row } ?? []
+ let selectedTunnels = selectedTunnelIndices.compactMap { tunnelIndex in
+ tunnelIndex >= 0 && tunnelIndex < tunnelsManager.numberOfTunnels() ? tunnelsManager.tunnel(at: tunnelIndex) : nil
+ }
+ guard !selectedTunnels.isEmpty else { return }
+ let message = selectedTunnels.count == 1 ?
+ tr(format: "deleteTunnelConfirmationAlertButtonMessage (%d)", selectedTunnels.count) :
+ tr(format: "deleteTunnelsConfirmationAlertButtonMessage (%d)", selectedTunnels.count)
+ let title = tr("deleteTunnelsConfirmationAlertButtonTitle")
+ ConfirmationAlertPresenter.showConfirmationAlert(message: message, buttonTitle: title,
+ from: sender, presentingVC: self) { [weak self] in
+ self?.tunnelsManager?.removeMultiple(tunnels: selectedTunnels) { [weak self] error in
+ guard let self = self else { return }
+ if let error = error {
+ ErrorPresenter.showErrorAlert(error: error, from: self)
+ return
+ }
+ self.tableState = .normal
+ self.tableView.setEditing(false, animated: true)
+ }
+ }
+ }
+
+ func showTunnelDetail(for tunnel: TunnelContainer, animated: Bool) {
+ guard let tunnelsManager = tunnelsManager else { return }
+ guard let splitViewController = splitViewController else { return }
+ guard let navController = navigationController else { return }
+
+ let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager,
+ tunnel: tunnel)
+ let tunnelDetailNC = UINavigationController(rootViewController: tunnelDetailVC)
+ tunnelDetailNC.restorationIdentifier = "DetailNC"
+ if splitViewController.isCollapsed && navController.viewControllers.count > 1 {
+ navController.setViewControllers([self, tunnelDetailNC], animated: animated)
+ } else {
+ splitViewController.showDetailViewController(tunnelDetailNC, sender: self, animated: animated)
+ }
+ detailDisplayedTunnel = tunnel
+ self.presentedViewController?.dismiss(animated: false, completion: nil)
+ }
+}
+
+extension TunnelsListTableViewController: UIDocumentPickerDelegate {
+ func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
+ guard let tunnelsManager = tunnelsManager else { return }
+ TunnelImporter.importFromFile(urls: urls, into: tunnelsManager, sourceVC: self, errorPresenterType: ErrorPresenter.self)
+ }
+}
+
+extension TunnelsListTableViewController: QRScanViewControllerDelegate {
+ func addScannedQRCode(tunnelConfiguration: TunnelConfiguration, qrScanViewController: QRScanViewController,
+ completionHandler: (() -> Void)?) {
+ tunnelsManager?.add(tunnelConfiguration: tunnelConfiguration) { result in
+ switch result {
+ case .failure(let error):
+ ErrorPresenter.showErrorAlert(error: error, from: qrScanViewController, onDismissal: completionHandler)
+ case .success:
+ completionHandler?()
+ }
+ }
+ }
+}
+
+extension TunnelsListTableViewController: UITableViewDataSource {
+ func numberOfSections(in tableView: UITableView) -> Int {
+ return 1
+ }
+
+ func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return (tunnelsManager?.numberOfTunnels() ?? 0)
+ }
+
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell: TunnelListCell = tableView.dequeueReusableCell(for: indexPath)
+ if let tunnelsManager = tunnelsManager {
+ let tunnel = tunnelsManager.tunnel(at: indexPath.row)
+ cell.tunnel = tunnel
+ cell.onSwitchToggled = { [weak self] isOn in
+ guard let self = self, let tunnelsManager = self.tunnelsManager else { return }
+ if isOn {
+ tunnelsManager.startActivation(of: tunnel)
+ } else {
+ tunnelsManager.startDeactivation(of: tunnel)
+ }
+ }
+ }
+ return cell
+ }
+}
+
+extension TunnelsListTableViewController: UITableViewDelegate {
+ func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ guard !tableView.isEditing else {
+ tableState = .multiSelect(selectionCount: tableView.indexPathsForSelectedRows?.count ?? 0)
+ return
+ }
+ guard let tunnelsManager = tunnelsManager else { return }
+ let tunnel = tunnelsManager.tunnel(at: indexPath.row)
+ showTunnelDetail(for: tunnel, animated: true)
+ }
+
+ func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
+ guard !tableView.isEditing else {
+ tableState = .multiSelect(selectionCount: tableView.indexPathsForSelectedRows?.count ?? 0)
+ return
+ }
+ }
+
+ func tableView(_ tableView: UITableView,
+ trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+ let deleteAction = UIContextualAction(style: .destructive, title: tr("tunnelsListSwipeDeleteButtonTitle")) { [weak self] _, _, completionHandler in
+ guard let tunnelsManager = self?.tunnelsManager else { return }
+ let tunnel = tunnelsManager.tunnel(at: indexPath.row)
+ tunnelsManager.remove(tunnel: tunnel) { error in
+ if error != nil {
+ ErrorPresenter.showErrorAlert(error: error!, from: self)
+ completionHandler(false)
+ } else {
+ completionHandler(true)
+ }
+ }
+ }
+ return UISwipeActionsConfiguration(actions: [deleteAction])
+ }
+
+ func tableView(_ tableView: UITableView, willBeginEditingRowAt indexPath: IndexPath) {
+ if tableState == .normal {
+ tableState = .rowSwiped
+ }
+ }
+
+ func tableView(_ tableView: UITableView, didEndEditingRowAt indexPath: IndexPath?) {
+ if tableState == .rowSwiped {
+ tableState = .normal
+ }
+ }
+}
+
+extension TunnelsListTableViewController: TunnelsManagerListDelegate {
+ func tunnelAdded(at index: Int) {
+ tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
+ centeredAddButton.isHidden = (tunnelsManager?.numberOfTunnels() ?? 0 > 0)
+ }
+
+ func tunnelModified(at index: Int) {
+ tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
+ }
+
+ func tunnelMoved(from oldIndex: Int, to newIndex: Int) {
+ tableView.moveRow(at: IndexPath(row: oldIndex, section: 0), to: IndexPath(row: newIndex, section: 0))
+ }
+
+ func tunnelRemoved(at index: Int, tunnel: TunnelContainer) {
+ tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
+ centeredAddButton.isHidden = tunnelsManager?.numberOfTunnels() ?? 0 > 0
+ if detailDisplayedTunnel == tunnel, let splitViewController = splitViewController {
+ if splitViewController.isCollapsed != false {
+ (splitViewController.viewControllers[0] as? UINavigationController)?.popToRootViewController(animated: false)
+ } else {
+ let detailVC = UIViewController()
+ if #available(iOS 13.0, *) {
+ detailVC.view.backgroundColor = .systemBackground
+ } else {
+ detailVC.view.backgroundColor = .white
+ }
+ let detailNC = UINavigationController(rootViewController: detailVC)
+ splitViewController.showDetailViewController(detailNC, sender: self)
+ }
+ detailDisplayedTunnel = nil
+ if let presentedNavController = self.presentedViewController as? UINavigationController, presentedNavController.viewControllers.first is TunnelEditTableViewController {
+ self.presentedViewController?.dismiss(animated: false, completion: nil)
+ }
+ }
+ }
+}
+
+extension UISplitViewController {
+ func showDetailViewController(_ vc: UIViewController, sender: Any?, animated: Bool) {
+ if animated {
+ showDetailViewController(vc, sender: sender)
+ } else {
+ UIView.performWithoutAnimation {
+ showDetailViewController(vc, sender: sender)
+ }
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/UI/iOS/WireGuard.entitlements b/Sources/WireGuardApp/UI/iOS/WireGuard.entitlements
new file mode 100644
index 0000000..93c7249
--- /dev/null
+++ b/Sources/WireGuardApp/UI/iOS/WireGuard.entitlements
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-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>com.apple.developer.networking.networkextension</key>
+ <array>
+ <string>packet-tunnel-provider</string>
+ </array>
+ <key>com.apple.developer.networking.wifi-info</key>
+ <true/>
+ <key>com.apple.security.application-groups</key>
+ <array>
+ <string>group.$(APP_ID_IOS)</string>
+ </array>
+</dict>
+</plist>
diff --git a/Sources/WireGuardApp/UI/macOS/AppDelegate.swift b/Sources/WireGuardApp/UI/macOS/AppDelegate.swift
new file mode 100644
index 0000000..6e3783a
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/AppDelegate.swift
@@ -0,0 +1,252 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+import ServiceManagement
+import WireGuardKit
+
+@NSApplicationMain
+class AppDelegate: NSObject, NSApplicationDelegate {
+
+ var tunnelsManager: TunnelsManager?
+ var tunnelsTracker: TunnelsTracker?
+ var statusItemController: StatusItemController?
+
+ var manageTunnelsRootVC: ManageTunnelsRootViewController?
+ var manageTunnelsWindowObject: NSWindow?
+ var onAppDeactivation: (() -> Void)?
+
+ func applicationWillFinishLaunching(_ notification: Notification) {
+ // To workaround a possible AppKit bug that causes the main menu to become unresponsive sometimes
+ // (especially when launched through Xcode) if we call setActivationPolicy(.regular) in
+ // in applicationDidFinishLaunching, we set it to .prohibited here.
+ // Setting it to .regular would fix that problem too, but at this point, we don't know
+ // whether the app was launched at login or not, so we're not sure whether we should
+ // show the app icon in the dock or not.
+ NSApp.setActivationPolicy(.prohibited)
+ }
+
+ func applicationDidFinishLaunching(_ aNotification: Notification) {
+ Logger.configureGlobal(tagged: "APP", withFilePath: FileManager.logFileURL?.path)
+ registerLoginItem(shouldLaunchAtLogin: true)
+
+ var isLaunchedAtLogin = false
+ if let appleEvent = NSAppleEventManager.shared().currentAppleEvent {
+ isLaunchedAtLogin = LaunchedAtLoginDetector.isLaunchedAtLogin(openAppleEvent: appleEvent)
+ }
+
+ NSApp.mainMenu = MainMenu()
+ setDockIconAndMainMenuVisibility(isVisible: !isLaunchedAtLogin)
+
+ TunnelsManager.create { [weak self] result in
+ guard let self = self else { return }
+
+ switch result {
+ case .failure(let error):
+ ErrorPresenter.showErrorAlert(error: error, from: nil)
+ case .success(let tunnelsManager):
+ let statusMenu = StatusMenu(tunnelsManager: tunnelsManager)
+ statusMenu.windowDelegate = self
+
+ let statusItemController = StatusItemController()
+ statusItemController.statusItem.menu = statusMenu
+
+ let tunnelsTracker = TunnelsTracker(tunnelsManager: tunnelsManager)
+ tunnelsTracker.statusMenu = statusMenu
+ tunnelsTracker.statusItemController = statusItemController
+
+ self.tunnelsManager = tunnelsManager
+ self.tunnelsTracker = tunnelsTracker
+ self.statusItemController = statusItemController
+
+ if !isLaunchedAtLogin {
+ self.showManageTunnelsWindow(completion: nil)
+ }
+ }
+ }
+ }
+
+ func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows: Bool) -> Bool {
+ if let appleEvent = NSAppleEventManager.shared().currentAppleEvent {
+ if LaunchedAtLoginDetector.isReopenedByLoginItemHelper(reopenAppleEvent: appleEvent) {
+ return false
+ }
+ }
+ if hasVisibleWindows {
+ return true
+ }
+ showManageTunnelsWindow(completion: nil)
+ return false
+ }
+
+ @objc func confirmAndQuit() {
+ let alert = NSAlert()
+ alert.messageText = tr("macConfirmAndQuitAlertMessage")
+ if let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating {
+ alert.informativeText = tr(format: "macConfirmAndQuitInfoWithActiveTunnel (%@)", currentTunnel.name)
+ } else {
+ alert.informativeText = tr("macConfirmAndQuitAlertInfo")
+ }
+ alert.addButton(withTitle: tr("macConfirmAndQuitAlertCloseWindow"))
+ alert.addButton(withTitle: tr("macConfirmAndQuitAlertQuitWireGuard"))
+
+ NSApp.activate(ignoringOtherApps: true)
+ if let manageWindow = manageTunnelsWindowObject {
+ manageWindow.orderFront(self)
+ alert.beginSheetModal(for: manageWindow) { response in
+ switch response {
+ case .alertFirstButtonReturn:
+ manageWindow.close()
+ case .alertSecondButtonReturn:
+ NSApp.terminate(nil)
+ default:
+ break
+ }
+ }
+ }
+ }
+
+ @objc func quit() {
+ if let manageWindow = manageTunnelsWindowObject, manageWindow.attachedSheet != nil {
+ NSApp.activate(ignoringOtherApps: true)
+ manageWindow.orderFront(self)
+ return
+ }
+ registerLoginItem(shouldLaunchAtLogin: false)
+ guard let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating else {
+ NSApp.terminate(nil)
+ return
+ }
+ let alert = NSAlert()
+ alert.messageText = tr("macAppExitingWithActiveTunnelMessage")
+ alert.informativeText = tr("macAppExitingWithActiveTunnelInfo")
+ NSApp.activate(ignoringOtherApps: true)
+ if let manageWindow = manageTunnelsWindowObject {
+ manageWindow.orderFront(self)
+ alert.beginSheetModal(for: manageWindow) { _ in
+ NSApp.terminate(nil)
+ }
+ } else {
+ alert.runModal()
+ NSApp.terminate(nil)
+ }
+ }
+
+ func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
+ guard let currentTunnel = tunnelsTracker?.currentTunnel, currentTunnel.status == .active || currentTunnel.status == .activating else {
+ return .terminateNow
+ }
+ guard let appleEvent = NSAppleEventManager.shared().currentAppleEvent else {
+ return .terminateNow
+ }
+ guard MacAppStoreUpdateDetector.isUpdatingFromMacAppStore(quitAppleEvent: appleEvent) else {
+ return .terminateNow
+ }
+ let alert = NSAlert()
+ alert.messageText = tr("macAppStoreUpdatingAlertMessage")
+ if currentTunnel.isActivateOnDemandEnabled {
+ alert.informativeText = tr(format: "macAppStoreUpdatingAlertInfoWithOnDemand (%@)", currentTunnel.name)
+ } else {
+ alert.informativeText = tr(format: "macAppStoreUpdatingAlertInfoWithoutOnDemand (%@)", currentTunnel.name)
+ }
+ NSApp.activate(ignoringOtherApps: true)
+ if let manageWindow = manageTunnelsWindowObject {
+ alert.beginSheetModal(for: manageWindow) { _ in }
+ } else {
+ alert.runModal()
+ }
+ return .terminateCancel
+ }
+
+ func applicationShouldTerminateAfterLastWindowClosed(_ application: NSApplication) -> Bool {
+ DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak self] in
+ self?.setDockIconAndMainMenuVisibility(isVisible: false)
+ }
+ return false
+ }
+
+ private func setDockIconAndMainMenuVisibility(isVisible: Bool, completion: (() -> Void)? = nil) {
+ let currentActivationPolicy = NSApp.activationPolicy()
+ let newActivationPolicy: NSApplication.ActivationPolicy = isVisible ? .regular : .accessory
+ guard currentActivationPolicy != newActivationPolicy else {
+ if newActivationPolicy == .regular {
+ NSApp.activate(ignoringOtherApps: true)
+ }
+ completion?()
+ return
+ }
+ if newActivationPolicy == .regular && NSApp.isActive {
+ // To workaround a possible AppKit bug that causes the main menu to become unresponsive,
+ // we should deactivate the app first and then set the activation policy.
+ // NSApp.deactivate() doesn't always deactivate the app, so we instead use
+ // setActivationPolicy(.prohibited).
+ onAppDeactivation = {
+ NSApp.setActivationPolicy(.regular)
+ NSApp.activate(ignoringOtherApps: true)
+ completion?()
+ }
+ NSApp.setActivationPolicy(.prohibited)
+ } else {
+ NSApp.setActivationPolicy(newActivationPolicy)
+ if newActivationPolicy == .regular {
+ NSApp.activate(ignoringOtherApps: true)
+ }
+ completion?()
+ }
+ }
+
+ func applicationDidResignActive(_ notification: Notification) {
+ onAppDeactivation?()
+ onAppDeactivation = nil
+ }
+}
+
+extension AppDelegate {
+ @objc func aboutClicked() {
+ var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
+ if let appBuild = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
+ appVersion += " (\(appBuild))"
+ }
+ let appVersionString = [
+ tr(format: "macAppVersion (%@)", appVersion),
+ tr(format: "macGoBackendVersion (%@)", wireGuardVersion)
+ ].joined(separator: "\n")
+ let donateString = NSMutableAttributedString(string: tr("donateLink"))
+ donateString.addAttribute(.link, value: "https://www.wireguard.com/donations/", range: NSRange(location: 0, length: donateString.length))
+ NSApp.activate(ignoringOtherApps: true)
+ NSApp.orderFrontStandardAboutPanel(options: [
+ .applicationVersion: appVersionString,
+ .version: "",
+ .credits: donateString
+ ])
+ }
+}
+
+extension AppDelegate: StatusMenuWindowDelegate {
+ func showManageTunnelsWindow(completion: ((NSWindow?) -> Void)?) {
+ guard let tunnelsManager = tunnelsManager else {
+ completion?(nil)
+ return
+ }
+ if manageTunnelsWindowObject == nil {
+ manageTunnelsRootVC = ManageTunnelsRootViewController(tunnelsManager: tunnelsManager)
+ let window = NSWindow(contentViewController: manageTunnelsRootVC!)
+ window.title = tr("macWindowTitleManageTunnels")
+ window.setContentSize(NSSize(width: 800, height: 480))
+ window.setFrameAutosaveName(NSWindow.FrameAutosaveName("ManageTunnelsWindow")) // Auto-save window position and size
+ manageTunnelsWindowObject = window
+ tunnelsTracker?.manageTunnelsRootVC = manageTunnelsRootVC
+ }
+ setDockIconAndMainMenuVisibility(isVisible: true) { [weak manageTunnelsWindowObject] in
+ manageTunnelsWindowObject?.makeKeyAndOrderFront(self)
+ completion?(manageTunnelsWindowObject)
+ }
+ }
+}
+
+@discardableResult
+func registerLoginItem(shouldLaunchAtLogin: Bool) -> Bool {
+ let appId = Bundle.main.bundleIdentifier!
+ let helperBundleId = "\(appId).login-item-helper"
+ return SMLoginItemSetEnabled(helperBundleId as CFString, shouldLaunchAtLogin)
+}
diff --git a/Sources/WireGuardApp/UI/macOS/Application.swift b/Sources/WireGuardApp/UI/macOS/Application.swift
new file mode 100644
index 0000000..0ce274a
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Application.swift
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class Application: NSApplication {
+
+ private var appDelegate: AppDelegate? //swiftlint:disable:this weak_delegate
+
+ override init() {
+ super.init()
+ appDelegate = AppDelegate() // Keep a strong reference to the app delegate
+ delegate = appDelegate // Set delegate before app.run() gets called in NSApplicationMain()
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..32ea528
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,68 @@
+{
+ "images" : [
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "WireGuardMacAppIcon16.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "16x16",
+ "idiom" : "mac",
+ "filename" : "WireGuardMacAppIcon32.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "WireGuardMacAppIcon32-1.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "32x32",
+ "idiom" : "mac",
+ "filename" : "WireGuardMacAppIcon64.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "WireGuardMacAppIcon128.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "128x128",
+ "idiom" : "mac",
+ "filename" : "WireGuardMacAppIcon256.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "WireGuardMacAppIcon256-1.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "256x256",
+ "idiom" : "mac",
+ "filename" : "WireGuardMacAppIcon512.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "WireGuardMacAppIcon512-1.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "512x512",
+ "idiom" : "mac",
+ "filename" : "WireGuardMacAppIcon.png",
+ "scale" : "2x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon.png
new file mode 100644
index 0000000..83c4701
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon128.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon128.png
new file mode 100644
index 0000000..16f9056
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon128.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon16.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon16.png
new file mode 100644
index 0000000..e05c706
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon16.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon256-1.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon256-1.png
new file mode 100644
index 0000000..ac09bdd
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon256-1.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon256.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon256.png
new file mode 100644
index 0000000..ac09bdd
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon256.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon32-1.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon32-1.png
new file mode 100644
index 0000000..edf0ca5
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon32-1.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon32.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon32.png
new file mode 100644
index 0000000..edf0ca5
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon32.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon512-1.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon512-1.png
new file mode 100644
index 0000000..54b28ff
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon512-1.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon512.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon512.png
new file mode 100644
index 0000000..54b28ff
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon512.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon64.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon64.png
new file mode 100644
index 0000000..98441a8
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/AppIcon.appiconset/WireGuardMacAppIcon64.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/Contents.json b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..da4a164
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+} \ No newline at end of file
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/Contents.json b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/Contents.json
new file mode 100644
index 0000000..a8cd607
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/Contents.json
@@ -0,0 +1,26 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIcon@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIcon@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIcon@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@1x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@1x.png
new file mode 100644
index 0000000..c0a43e7
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@1x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@2x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@2x.png
new file mode 100644
index 0000000..2057c31
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@2x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@3x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@3x.png
new file mode 100644
index 0000000..60cc363
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIcon.imageset/StatusBarIcon@3x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/Contents.json b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/Contents.json
new file mode 100644
index 0000000..f9e2cd6
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/Contents.json
@@ -0,0 +1,26 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDimmed@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDimmed@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDimmed@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@1x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@1x.png
new file mode 100644
index 0000000..fb9d8f7
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@1x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@2x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@2x.png
new file mode 100644
index 0000000..2f4e613
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@2x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@3x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@3x.png
new file mode 100644
index 0000000..cc5ead9
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDimmed.imageset/StatusBarIconDimmed@3x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/Contents.json b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/Contents.json
new file mode 100644
index 0000000..15384b9
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/Contents.json
@@ -0,0 +1,26 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot1@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot1@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot1@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/StatusBarIconDot1@1x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/StatusBarIconDot1@1x.png
new file mode 100644
index 0000000..bbbe0c3
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/StatusBarIconDot1@1x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/StatusBarIconDot1@2x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/StatusBarIconDot1@2x.png
new file mode 100644
index 0000000..01b5eb3
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/StatusBarIconDot1@2x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/StatusBarIconDot1@3x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/StatusBarIconDot1@3x.png
new file mode 100644
index 0000000..76afa15
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot1.imageset/StatusBarIconDot1@3x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/Contents.json b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/Contents.json
new file mode 100644
index 0000000..f728363
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/Contents.json
@@ -0,0 +1,26 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot2@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot2@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot2@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/StatusBarIconDot2@1x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/StatusBarIconDot2@1x.png
new file mode 100644
index 0000000..a16143f
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/StatusBarIconDot2@1x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/StatusBarIconDot2@2x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/StatusBarIconDot2@2x.png
new file mode 100644
index 0000000..ce00482
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/StatusBarIconDot2@2x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/StatusBarIconDot2@3x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/StatusBarIconDot2@3x.png
new file mode 100644
index 0000000..82640f7
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot2.imageset/StatusBarIconDot2@3x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/Contents.json b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/Contents.json
new file mode 100644
index 0000000..eeaa5d1
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/Contents.json
@@ -0,0 +1,26 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot3@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot3@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "StatusBarIconDot3@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ },
+ "properties" : {
+ "template-rendering-intent" : "template"
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/StatusBarIconDot3@1x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/StatusBarIconDot3@1x.png
new file mode 100644
index 0000000..80e221b
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/StatusBarIconDot3@1x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/StatusBarIconDot3@2x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/StatusBarIconDot3@2x.png
new file mode 100644
index 0000000..663f92b
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/StatusBarIconDot3@2x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/StatusBarIconDot3@3x.png b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/StatusBarIconDot3@3x.png
new file mode 100644
index 0000000..10e43d9
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Assets.xcassets/StatusBarIconDot3.imageset/StatusBarIconDot3@3x.png
Binary files differ
diff --git a/Sources/WireGuardApp/UI/macOS/ErrorPresenter.swift b/Sources/WireGuardApp/UI/macOS/ErrorPresenter.swift
new file mode 100644
index 0000000..1eb2b04
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/ErrorPresenter.swift
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class ErrorPresenter: ErrorPresenterProtocol {
+ static func showErrorAlert(title: String, message: String, from sourceVC: AnyObject?, onPresented: (() -> Void)?, onDismissal: (() -> Void)?) {
+ let alert = NSAlert()
+ alert.messageText = title
+ alert.informativeText = message
+ onPresented?()
+ if let sourceVC = sourceVC as? NSViewController {
+ NSApp.activate(ignoringOtherApps: true)
+ sourceVC.view.window!.makeKeyAndOrderFront(nil)
+ alert.beginSheetModal(for: sourceVC.view.window!) { _ in
+ onDismissal?()
+ }
+ } else {
+ alert.runModal()
+ onDismissal?()
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/ImportPanelPresenter.swift b/Sources/WireGuardApp/UI/macOS/ImportPanelPresenter.swift
new file mode 100644
index 0000000..d081f8c
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/ImportPanelPresenter.swift
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class ImportPanelPresenter {
+ static func presentImportPanel(tunnelsManager: TunnelsManager, sourceVC: NSViewController?) {
+ guard let window = sourceVC?.view.window else { return }
+ let openPanel = NSOpenPanel()
+ openPanel.prompt = tr("macSheetButtonImport")
+ openPanel.allowedFileTypes = ["conf", "zip"]
+ openPanel.allowsMultipleSelection = true
+ openPanel.beginSheetModal(for: window) { [weak tunnelsManager] response in
+ guard let tunnelsManager = tunnelsManager else { return }
+ guard response == .OK else { return }
+ TunnelImporter.importFromFile(urls: openPanel.urls, into: tunnelsManager, sourceVC: sourceVC, errorPresenterType: ErrorPresenter.self)
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/Info.plist b/Sources/WireGuardApp/UI/macOS/Info.plist
new file mode 100644
index 0000000..db1afeb
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/Info.plist
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-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>ITSAppUsesNonExemptEncryption</key>
+ <false/>
+ <key>LSMultipleInstancesProhibited</key>
+ <true/>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIconFile</key>
+ <string></string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>$(VERSION_NAME)</string>
+ <key>CFBundleVersion</key>
+ <string>$(VERSION_ID)</string>
+ <key>LSMinimumSystemVersion</key>
+ <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
+ <key>LSUIElement</key>
+ <true/>
+ <key>NSHumanReadableCopyright</key>
+ <string>Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.</string>
+ <key>NSPrincipalClass</key>
+ <string>WireGuard.Application</string>
+ <key>LSApplicationCategoryType</key>
+ <string>public.app-category.utilities</string>
+ <key>com.wireguard.macos.app_group_id</key>
+ <string>$(DEVELOPMENT_TEAM).group.$(APP_ID_MACOS)</string>
+</dict>
+</plist>
diff --git a/Sources/WireGuardApp/UI/macOS/LaunchedAtLoginDetector.swift b/Sources/WireGuardApp/UI/macOS/LaunchedAtLoginDetector.swift
new file mode 100644
index 0000000..0d8e3d8
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/LaunchedAtLoginDetector.swift
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class LaunchedAtLoginDetector {
+ static let launchCode = "LaunchedByWireGuardLoginItemHelper"
+
+ static func isLaunchedAtLogin(openAppleEvent: NSAppleEventDescriptor) -> Bool {
+ guard isOpenEvent(openAppleEvent) else { return false }
+ guard let propData = openAppleEvent.paramDescriptor(forKeyword: keyAEPropData) else { return false }
+ return propData.stringValue == launchCode
+ }
+
+ static func isReopenedByLoginItemHelper(reopenAppleEvent: NSAppleEventDescriptor) -> Bool {
+ guard isReopenEvent(reopenAppleEvent) else { return false }
+ guard let propData = reopenAppleEvent.paramDescriptor(forKeyword: keyAEPropData) else { return false }
+ return propData.stringValue == launchCode
+ }
+}
+
+private func isOpenEvent(_ event: NSAppleEventDescriptor) -> Bool {
+ return event.eventClass == kCoreEventClass && event.eventID == kAEOpenApplication
+}
+
+private func isReopenEvent(_ event: NSAppleEventDescriptor) -> Bool {
+ return event.eventClass == kCoreEventClass && event.eventID == kAEReopenApplication
+}
diff --git a/Sources/WireGuardApp/UI/macOS/LoginItemHelper/Info.plist b/Sources/WireGuardApp/UI/macOS/LoginItemHelper/Info.plist
new file mode 100644
index 0000000..7ddff91
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/LoginItemHelper/Info.plist
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-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>ITSAppUsesNonExemptEncryption</key>
+ <false/>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
+ <key>CFBundleExecutable</key>
+ <string>$(EXECUTABLE_NAME)</string>
+ <key>CFBundleIconFile</key>
+ <string></string>
+ <key>CFBundleIdentifier</key>
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundleName</key>
+ <string>$(PRODUCT_NAME)</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleShortVersionString</key>
+ <string>$(VERSION_NAME)</string>
+ <key>CFBundleVersion</key>
+ <string>$(VERSION_ID)</string>
+ <key>LSMinimumSystemVersion</key>
+ <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
+ <key>NSHumanReadableCopyright</key>
+ <string>Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.</string>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+ <key>LSBackgroundOnly</key>
+ <true/>
+ <key>com.wireguard.macos.app_id</key>
+ <string>$(APP_ID_MACOS)</string>
+</dict>
+</plist>
diff --git a/Sources/WireGuardApp/UI/macOS/LoginItemHelper/LoginItemHelper.entitlements b/Sources/WireGuardApp/UI/macOS/LoginItemHelper/LoginItemHelper.entitlements
new file mode 100644
index 0000000..852fa1a
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/LoginItemHelper/LoginItemHelper.entitlements
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-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>com.apple.security.app-sandbox</key>
+ <true/>
+</dict>
+</plist>
diff --git a/Sources/WireGuardApp/UI/macOS/LoginItemHelper/main.m b/Sources/WireGuardApp/UI/macOS/LoginItemHelper/main.m
new file mode 100644
index 0000000..1010b49
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/LoginItemHelper/main.m
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+#import <Cocoa/Cocoa.h>
+
+int main(int argc, char *argv[])
+{
+ NSString *appIdInfoDictionaryKey = @"com.wireguard.macos.app_id";
+ NSString *appId = [NSBundle.mainBundle objectForInfoDictionaryKey:appIdInfoDictionaryKey];
+
+ NSString *launchCode = @"LaunchedByWireGuardLoginItemHelper";
+ NSAppleEventDescriptor *paramDescriptor = [NSAppleEventDescriptor descriptorWithString:launchCode];
+
+ [NSWorkspace.sharedWorkspace launchAppWithBundleIdentifier:appId options:NSWorkspaceLaunchWithoutActivation
+ additionalEventParamDescriptor:paramDescriptor launchIdentifier:NULL];
+ return 0;
+}
diff --git a/Sources/WireGuardApp/UI/macOS/MacAppStoreUpdateDetector.swift b/Sources/WireGuardApp/UI/macOS/MacAppStoreUpdateDetector.swift
new file mode 100644
index 0000000..68608ca
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/MacAppStoreUpdateDetector.swift
@@ -0,0 +1,35 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class MacAppStoreUpdateDetector {
+ static func isUpdatingFromMacAppStore(quitAppleEvent: NSAppleEventDescriptor) -> Bool {
+ guard isQuitEvent(quitAppleEvent) else { return false }
+ guard let senderPIDDescriptor = quitAppleEvent.attributeDescriptor(forKeyword: keySenderPIDAttr) else { return false }
+ let pid = senderPIDDescriptor.int32Value
+ guard let executablePath = getExecutablePath(from: pid) else { return false }
+ wg_log(.debug, message: "aevt/quit Apple event received from: \(executablePath)")
+ if executablePath.hasPrefix("/System/Library/") {
+ let executableName = URL(fileURLWithPath: executablePath, isDirectory: false).lastPathComponent
+ return executableName == "com.apple.CommerceKit.StoreAEService"
+ }
+ return false
+ }
+}
+
+private func isQuitEvent(_ event: NSAppleEventDescriptor) -> Bool {
+ return event.eventClass == kCoreEventClass && event.eventID == kAEQuitApplication
+}
+
+private func getExecutablePath(from pid: pid_t) -> String? {
+ let bufferSize = Int(PATH_MAX)
+ var buffer = Data(capacity: bufferSize)
+ return buffer.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) -> String? in
+ if let basePtr = ptr.baseAddress {
+ let byteCount = proc_pidpath(pid, basePtr, UInt32(bufferSize))
+ return byteCount > 0 ? String(cString: basePtr.bindMemory(to: CChar.self, capacity: bufferSize)) : nil
+ }
+ return nil
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/MainMenu.swift b/Sources/WireGuardApp/UI/macOS/MainMenu.swift
new file mode 100644
index 0000000..92fca4b
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/MainMenu.swift
@@ -0,0 +1,130 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+// swiftlint:disable colon
+
+class MainMenu: NSMenu {
+ init() {
+ super.init(title: "")
+ addSubmenu(createApplicationMenu())
+ addSubmenu(createFileMenu())
+ addSubmenu(createEditMenu())
+ addSubmenu(createTunnelMenu())
+ addSubmenu(createWindowMenu())
+ }
+
+ required init(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ private func addSubmenu(_ menu: NSMenu) {
+ let menuItem = self.addItem(withTitle: "", action: nil, keyEquivalent: "")
+ self.setSubmenu(menu, for: menuItem)
+ }
+
+ private func createApplicationMenu() -> NSMenu {
+ let menu = NSMenu()
+
+ let aboutMenuItem = menu.addItem(withTitle: tr("macMenuAbout"),
+ action: #selector(AppDelegate.aboutClicked), keyEquivalent: "")
+ aboutMenuItem.target = NSApp.delegate
+
+ menu.addItem(NSMenuItem.separator())
+
+ menu.addItem(withTitle: tr("macMenuViewLog"),
+ action: #selector(TunnelsListTableViewController.handleViewLogAction), keyEquivalent: "")
+
+ menu.addItem(NSMenuItem.separator())
+
+ let hideMenuItem = menu.addItem(withTitle: tr("macMenuHideApp"),
+ action: #selector(NSApplication.hide), keyEquivalent: "h")
+ hideMenuItem.target = NSApp
+ let hideOthersMenuItem = menu.addItem(withTitle: tr("macMenuHideOtherApps"),
+ action: #selector(NSApplication.hideOtherApplications), keyEquivalent: "h")
+ hideOthersMenuItem.keyEquivalentModifierMask = [.command, .option]
+ hideOthersMenuItem.target = NSApp
+ let showAllMenuItem = menu.addItem(withTitle: tr("macMenuShowAllApps"),
+ action: #selector(NSApplication.unhideAllApplications), keyEquivalent: "")
+ showAllMenuItem.target = NSApp
+
+ menu.addItem(NSMenuItem.separator())
+
+ menu.addItem(withTitle: tr("macMenuQuit"),
+ action: #selector(AppDelegate.confirmAndQuit), keyEquivalent: "q")
+
+ return menu
+ }
+
+ private func createFileMenu() -> NSMenu {
+ let menu = NSMenu(title: tr("macMenuFile"))
+
+ menu.addItem(withTitle: tr("macMenuAddEmptyTunnel"),
+ action: #selector(TunnelsListTableViewController.handleAddEmptyTunnelAction), keyEquivalent: "n")
+ menu.addItem(withTitle: tr("macMenuImportTunnels"),
+ action: #selector(TunnelsListTableViewController.handleImportTunnelAction), keyEquivalent: "o")
+
+ menu.addItem(NSMenuItem.separator())
+
+ menu.addItem(withTitle: tr("macMenuExportTunnels"),
+ action: #selector(TunnelsListTableViewController.handleExportTunnelsAction), keyEquivalent: "")
+
+ menu.addItem(NSMenuItem.separator())
+
+ menu.addItem(withTitle: tr("macMenuCloseWindow"), action: #selector(NSWindow.performClose(_:)), keyEquivalent:"w")
+
+ return menu
+ }
+
+ private func createEditMenu() -> NSMenu {
+ let menu = NSMenu(title: tr("macMenuEdit"))
+
+ menu.addItem(withTitle: "", action: #selector(UndoActionRespondable.undo(_:)), keyEquivalent:"z")
+ menu.addItem(withTitle: "", action: #selector(UndoActionRespondable.redo(_:)), keyEquivalent:"Z")
+
+ menu.addItem(NSMenuItem.separator())
+
+ menu.addItem(withTitle: tr("macMenuCut"), action: #selector(NSText.cut(_:)), keyEquivalent:"x")
+ menu.addItem(withTitle: tr("macMenuCopy"), action: #selector(NSText.copy(_:)), keyEquivalent:"c")
+ menu.addItem(withTitle: tr("macMenuPaste"), action: #selector(NSText.paste(_:)), keyEquivalent:"v")
+
+ menu.addItem(NSMenuItem.separator())
+
+ menu.addItem(withTitle: tr("macMenuSelectAll"), action: #selector(NSText.selectAll(_:)), keyEquivalent:"a")
+
+ return menu
+ }
+
+ private func createTunnelMenu() -> NSMenu {
+ let menu = NSMenu(title: tr("macMenuTunnel"))
+
+ menu.addItem(withTitle: tr("macMenuToggleStatus"), action: #selector(TunnelDetailTableViewController.handleToggleActiveStatusAction), keyEquivalent:"t")
+
+ menu.addItem(NSMenuItem.separator())
+
+ menu.addItem(withTitle: tr("macMenuEditTunnel"), action: #selector(TunnelDetailTableViewController.handleEditTunnelAction), keyEquivalent:"e")
+ menu.addItem(withTitle: tr("macMenuDeleteSelected"), action: #selector(TunnelsListTableViewController.handleRemoveTunnelAction), keyEquivalent: "")
+
+ return menu
+ }
+
+ private func createWindowMenu() -> NSMenu {
+ let menu = NSMenu(title: tr("macMenuWindow"))
+
+ menu.addItem(withTitle: tr("macMenuMinimize"), action: #selector(NSWindow.performMiniaturize(_:)), keyEquivalent:"m")
+ menu.addItem(withTitle: tr("macMenuZoom"), action: #selector(NSWindow.performZoom(_:)), keyEquivalent:"")
+
+ menu.addItem(NSMenuItem.separator())
+
+ let fullScreenMenuItem = menu.addItem(withTitle: "", action: #selector(NSWindow.toggleFullScreen(_:)), keyEquivalent:"f")
+ fullScreenMenuItem.keyEquivalentModifierMask = [.command, .control]
+
+ return menu
+ }
+}
+
+@objc protocol UndoActionRespondable {
+ func undo(_ sender: AnyObject)
+ func redo(_ sender: AnyObject)
+}
diff --git a/Sources/WireGuardApp/UI/macOS/NSColor+Hex.swift b/Sources/WireGuardApp/UI/macOS/NSColor+Hex.swift
new file mode 100644
index 0000000..4ca4f05
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/NSColor+Hex.swift
@@ -0,0 +1,25 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import AppKit
+
+extension NSColor {
+
+ convenience init(hex: String) {
+ var hexString = hex.uppercased()
+
+ if hexString.hasPrefix("#") {
+ hexString.remove(at: hexString.startIndex)
+ }
+
+ if hexString.count != 6 {
+ fatalError("Invalid hex string \(hex)")
+ }
+
+ var rgb: UInt32 = 0
+ Scanner(string: hexString).scanHexInt32(&rgb)
+
+ self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat((rgb >> 0) & 0xff) / 255.0, alpha: 1)
+ }
+
+}
diff --git a/Sources/WireGuardApp/UI/macOS/NSTableView+Reuse.swift b/Sources/WireGuardApp/UI/macOS/NSTableView+Reuse.swift
new file mode 100644
index 0000000..979b123
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/NSTableView+Reuse.swift
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+extension NSTableView {
+ func dequeueReusableCell<T: NSView>() -> T {
+ let identifier = NSUserInterfaceItemIdentifier(NSStringFromClass(T.self))
+ if let cellView = makeView(withIdentifier: identifier, owner: self) {
+ //swiftlint:disable:next force_cast
+ return cellView as! T
+ }
+ let cellView = T()
+ cellView.identifier = identifier
+ return cellView
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/ParseError+WireGuardAppError.swift b/Sources/WireGuardApp/UI/macOS/ParseError+WireGuardAppError.swift
new file mode 100644
index 0000000..8622510
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/ParseError+WireGuardAppError.swift
@@ -0,0 +1,57 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+import WireGuardKit
+
+// We have this in a separate file because we don't want the network extension
+// code to see WireGuardAppError and tr(). Also, this extension is used only on macOS.
+
+extension TunnelConfiguration.ParseError: WireGuardAppError {
+ var alertText: AlertText {
+ switch self {
+ case .invalidLine(let line):
+ return (tr(format: "macAlertInvalidLine (%@)", String(line)), "")
+ case .noInterface:
+ return (tr("macAlertNoInterface"), "")
+ case .multipleInterfaces:
+ return (tr("macAlertMultipleInterfaces"), "")
+ case .interfaceHasNoPrivateKey:
+ return (tr("alertInvalidInterfaceMessagePrivateKeyRequired"), tr("alertInvalidInterfaceMessagePrivateKeyInvalid"))
+ case .interfaceHasInvalidPrivateKey:
+ return (tr("macAlertPrivateKeyInvalid"), tr("alertInvalidInterfaceMessagePrivateKeyInvalid"))
+ case .interfaceHasInvalidListenPort(let value):
+ return (tr(format: "macAlertListenPortInvalid (%@)", value), tr("alertInvalidInterfaceMessageListenPortInvalid"))
+ case .interfaceHasInvalidAddress(let value):
+ return (tr(format: "macAlertAddressInvalid (%@)", value), tr("alertInvalidInterfaceMessageAddressInvalid"))
+ case .interfaceHasInvalidDNS(let value):
+ return (tr(format: "macAlertDNSInvalid (%@)", value), tr("alertInvalidInterfaceMessageDNSInvalid"))
+ case .interfaceHasInvalidMTU(let value):
+ return (tr(format: "macAlertMTUInvalid (%@)", value), tr("alertInvalidInterfaceMessageMTUInvalid"))
+ case .interfaceHasUnrecognizedKey(let value):
+ return (tr(format: "macAlertUnrecognizedInterfaceKey (%@)", value), tr("macAlertInfoUnrecognizedInterfaceKey"))
+ case .peerHasNoPublicKey:
+ return (tr("alertInvalidPeerMessagePublicKeyRequired"), tr("alertInvalidPeerMessagePublicKeyInvalid"))
+ case .peerHasInvalidPublicKey:
+ return (tr("macAlertPublicKeyInvalid"), tr("alertInvalidPeerMessagePublicKeyInvalid"))
+ case .peerHasInvalidPreSharedKey:
+ return (tr("macAlertPreSharedKeyInvalid"), tr("alertInvalidPeerMessagePreSharedKeyInvalid"))
+ case .peerHasInvalidAllowedIP(let value):
+ return (tr(format: "macAlertAllowedIPInvalid (%@)", value), tr("alertInvalidPeerMessageAllowedIPsInvalid"))
+ case .peerHasInvalidEndpoint(let value):
+ return (tr(format: "macAlertEndpointInvalid (%@)", value), tr("alertInvalidPeerMessageEndpointInvalid"))
+ case .peerHasInvalidPersistentKeepAlive(let value):
+ return (tr(format: "macAlertPersistentKeepliveInvalid (%@)", value), tr("alertInvalidPeerMessagePersistentKeepaliveInvalid"))
+ case .peerHasUnrecognizedKey(let value):
+ return (tr(format: "macAlertUnrecognizedPeerKey (%@)", value), tr("macAlertInfoUnrecognizedPeerKey"))
+ case .peerHasInvalidTransferBytes(let line):
+ return (tr(format: "macAlertInvalidLine (%@)", String(line)), "")
+ case .peerHasInvalidLastHandshakeTime(let line):
+ return (tr(format: "macAlertInvalidLine (%@)", String(line)), "")
+ case .multiplePeersWithSamePublicKey:
+ return (tr("alertInvalidPeerMessagePublicKeyDuplicated"), "")
+ case .multipleEntriesForKey(let value):
+ return (tr(format: "macAlertMultipleEntriesForKey (%@)", value), "")
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/StatusItemController.swift b/Sources/WireGuardApp/UI/macOS/StatusItemController.swift
new file mode 100644
index 0000000..ef0ccd0
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/StatusItemController.swift
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class StatusItemController {
+ var currentTunnel: TunnelContainer? {
+ didSet {
+ updateStatusItemImage()
+ }
+ }
+
+ let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
+ private let statusBarImageWhenActive = NSImage(named: "StatusBarIcon")!
+ private let statusBarImageWhenInactive = NSImage(named: "StatusBarIconDimmed")!
+
+ private let animationImages = [
+ NSImage(named: "StatusBarIconDot1")!,
+ NSImage(named: "StatusBarIconDot2")!,
+ NSImage(named: "StatusBarIconDot3")!
+ ]
+ private var animationImageIndex: Int = 0
+ private var animationTimer: Timer?
+
+ init() {
+ updateStatusItemImage()
+ }
+
+ func updateStatusItemImage() {
+ guard let currentTunnel = currentTunnel else {
+ stopActivatingAnimation()
+ statusItem.button?.image = statusBarImageWhenInactive
+ return
+ }
+ switch currentTunnel.status {
+ case .inactive:
+ stopActivatingAnimation()
+ statusItem.button?.image = statusBarImageWhenInactive
+ case .active:
+ stopActivatingAnimation()
+ statusItem.button?.image = statusBarImageWhenActive
+ case .activating, .waiting, .reasserting, .restarting, .deactivating:
+ startActivatingAnimation()
+ }
+ }
+
+ func startActivatingAnimation() {
+ guard animationTimer == nil else { return }
+ let timer = Timer(timeInterval: 0.3, repeats: true) { [weak self] _ in
+ guard let self = self else { return }
+ self.statusItem.button?.image = self.animationImages[self.animationImageIndex]
+ self.animationImageIndex = (self.animationImageIndex + 1) % self.animationImages.count
+ }
+ RunLoop.main.add(timer, forMode: .common)
+ animationTimer = timer
+ }
+
+ func stopActivatingAnimation() {
+ guard let timer = self.animationTimer else { return }
+ timer.invalidate()
+ animationTimer = nil
+ animationImageIndex = 0
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/StatusMenu.swift b/Sources/WireGuardApp/UI/macOS/StatusMenu.swift
new file mode 100644
index 0000000..ec9ffe8
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/StatusMenu.swift
@@ -0,0 +1,234 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+protocol StatusMenuWindowDelegate: class {
+ func showManageTunnelsWindow(completion: ((NSWindow?) -> Void)?)
+}
+
+class StatusMenu: NSMenu {
+
+ let tunnelsManager: TunnelsManager
+
+ var statusMenuItem: NSMenuItem?
+ var networksMenuItem: NSMenuItem?
+ var deactivateMenuItem: NSMenuItem?
+ var firstTunnelMenuItemIndex = 0
+ var numberOfTunnelMenuItems = 0
+
+ var currentTunnel: TunnelContainer? {
+ didSet {
+ updateStatusMenuItems(with: currentTunnel)
+ }
+ }
+ weak var windowDelegate: StatusMenuWindowDelegate?
+
+ init(tunnelsManager: TunnelsManager) {
+ self.tunnelsManager = tunnelsManager
+ super.init(title: tr("macMenuTitle"))
+
+ addStatusMenuItems()
+ addItem(NSMenuItem.separator())
+
+ firstTunnelMenuItemIndex = numberOfItems
+ let isAdded = addTunnelMenuItems()
+ if isAdded {
+ addItem(NSMenuItem.separator())
+ }
+ addTunnelManagementItems()
+ addItem(NSMenuItem.separator())
+ addApplicationItems()
+ }
+
+ required init(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func addStatusMenuItems() {
+ let statusTitle = tr(format: "macStatus (%@)", tr("tunnelStatusInactive"))
+ let statusMenuItem = NSMenuItem(title: statusTitle, action: nil, keyEquivalent: "")
+ statusMenuItem.isEnabled = false
+ addItem(statusMenuItem)
+ let networksMenuItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
+ networksMenuItem.isEnabled = false
+ networksMenuItem.isHidden = true
+ addItem(networksMenuItem)
+ let deactivateMenuItem = NSMenuItem(title: tr("macToggleStatusButtonDeactivate"), action: #selector(deactivateClicked), keyEquivalent: "")
+ deactivateMenuItem.target = self
+ deactivateMenuItem.isHidden = true
+ addItem(deactivateMenuItem)
+ self.statusMenuItem = statusMenuItem
+ self.networksMenuItem = networksMenuItem
+ self.deactivateMenuItem = deactivateMenuItem
+ }
+
+ func updateStatusMenuItems(with tunnel: TunnelContainer?) {
+ guard let statusMenuItem = statusMenuItem, let networksMenuItem = networksMenuItem, let deactivateMenuItem = deactivateMenuItem else { return }
+ guard let tunnel = tunnel else {
+ statusMenuItem.title = tr(format: "macStatus (%@)", tr("tunnelStatusInactive"))
+ networksMenuItem.title = ""
+ networksMenuItem.isHidden = true
+ deactivateMenuItem.isHidden = true
+ return
+ }
+ var statusText: String
+
+ switch tunnel.status {
+ case .waiting:
+ statusText = tr("tunnelStatusWaiting")
+ case .inactive:
+ statusText = tr("tunnelStatusInactive")
+ case .activating:
+ statusText = tr("tunnelStatusActivating")
+ case .active:
+ statusText = tr("tunnelStatusActive")
+ case .deactivating:
+ statusText = tr("tunnelStatusDeactivating")
+ case .reasserting:
+ statusText = tr("tunnelStatusReasserting")
+ case .restarting:
+ statusText = tr("tunnelStatusRestarting")
+ }
+
+ statusMenuItem.title = tr(format: "macStatus (%@)", statusText)
+
+ if tunnel.status == .inactive {
+ networksMenuItem.title = ""
+ networksMenuItem.isHidden = true
+ } else {
+ let allowedIPs = tunnel.tunnelConfiguration?.peers.flatMap { $0.allowedIPs }.map { $0.stringRepresentation }.joined(separator: ", ") ?? ""
+ if !allowedIPs.isEmpty {
+ networksMenuItem.title = tr(format: "macMenuNetworks (%@)", allowedIPs)
+ } else {
+ networksMenuItem.title = tr("macMenuNetworksNone")
+ }
+ networksMenuItem.isHidden = false
+ }
+ deactivateMenuItem.isHidden = tunnel.status != .active
+ }
+
+ func addTunnelMenuItems() -> Bool {
+ let numberOfTunnels = tunnelsManager.numberOfTunnels()
+ for index in 0 ..< tunnelsManager.numberOfTunnels() {
+ let tunnel = tunnelsManager.tunnel(at: index)
+ insertTunnelMenuItem(for: tunnel, at: numberOfTunnelMenuItems)
+ }
+ return numberOfTunnels > 0
+ }
+
+ func addTunnelManagementItems() {
+ let manageItem = NSMenuItem(title: tr("macMenuManageTunnels"), action: #selector(manageTunnelsClicked), keyEquivalent: "")
+ manageItem.target = self
+ addItem(manageItem)
+ let importItem = NSMenuItem(title: tr("macMenuImportTunnels"), action: #selector(importTunnelsClicked), keyEquivalent: "")
+ importItem.target = self
+ addItem(importItem)
+ }
+
+ func addApplicationItems() {
+ let aboutItem = NSMenuItem(title: tr("macMenuAbout"), action: #selector(AppDelegate.aboutClicked), keyEquivalent: "")
+ aboutItem.target = NSApp.delegate
+ addItem(aboutItem)
+ let quitItem = NSMenuItem(title: tr("macMenuQuit"), action: #selector(AppDelegate.quit), keyEquivalent: "")
+ quitItem.target = NSApp.delegate
+ addItem(quitItem)
+ }
+
+ @objc func deactivateClicked() {
+ if let currentTunnel = currentTunnel {
+ tunnelsManager.startDeactivation(of: currentTunnel)
+ }
+ }
+
+ @objc func tunnelClicked(sender: AnyObject) {
+ guard let tunnelMenuItem = sender as? TunnelMenuItem else { return }
+ if tunnelMenuItem.state == .off {
+ tunnelsManager.startActivation(of: tunnelMenuItem.tunnel)
+ } else {
+ tunnelsManager.startDeactivation(of: tunnelMenuItem.tunnel)
+ }
+ }
+
+ @objc func manageTunnelsClicked() {
+ windowDelegate?.showManageTunnelsWindow(completion: nil)
+ }
+
+ @objc func importTunnelsClicked() {
+ windowDelegate?.showManageTunnelsWindow { [weak self] manageTunnelsWindow in
+ guard let self = self else { return }
+ guard let manageTunnelsWindow = manageTunnelsWindow else { return }
+ ImportPanelPresenter.presentImportPanel(tunnelsManager: self.tunnelsManager,
+ sourceVC: manageTunnelsWindow.contentViewController)
+ }
+ }
+}
+
+extension StatusMenu {
+ func insertTunnelMenuItem(for tunnel: TunnelContainer, at tunnelIndex: Int) {
+ let menuItem = TunnelMenuItem(tunnel: tunnel, action: #selector(tunnelClicked(sender:)))
+ menuItem.target = self
+ menuItem.isHidden = !tunnel.isTunnelAvailableToUser
+ insertItem(menuItem, at: firstTunnelMenuItemIndex + tunnelIndex)
+ if numberOfTunnelMenuItems == 0 {
+ insertItem(NSMenuItem.separator(), at: firstTunnelMenuItemIndex + tunnelIndex + 1)
+ }
+ numberOfTunnelMenuItems += 1
+ }
+
+ func removeTunnelMenuItem(at tunnelIndex: Int) {
+ removeItem(at: firstTunnelMenuItemIndex + tunnelIndex)
+ numberOfTunnelMenuItems -= 1
+ if numberOfTunnelMenuItems == 0 {
+ if let firstItem = item(at: firstTunnelMenuItemIndex), firstItem.isSeparatorItem {
+ removeItem(at: firstTunnelMenuItemIndex)
+ }
+ }
+ }
+
+ func moveTunnelMenuItem(from oldTunnelIndex: Int, to newTunnelIndex: Int) {
+ guard let oldMenuItem = item(at: firstTunnelMenuItemIndex + oldTunnelIndex) as? TunnelMenuItem else { return }
+ let oldMenuItemTunnel = oldMenuItem.tunnel
+ removeItem(at: firstTunnelMenuItemIndex + oldTunnelIndex)
+ let menuItem = TunnelMenuItem(tunnel: oldMenuItemTunnel, action: #selector(tunnelClicked(sender:)))
+ menuItem.target = self
+ insertItem(menuItem, at: firstTunnelMenuItemIndex + newTunnelIndex)
+
+ }
+}
+
+class TunnelMenuItem: NSMenuItem {
+
+ var tunnel: TunnelContainer
+
+ private var statusObservationToken: AnyObject?
+ private var nameObservationToken: AnyObject?
+
+ init(tunnel: TunnelContainer, action selector: Selector?) {
+ self.tunnel = tunnel
+ super.init(title: tunnel.name, action: selector, keyEquivalent: "")
+ updateStatus()
+ let statusObservationToken = tunnel.observe(\.status) { [weak self] _, _ in
+ self?.updateStatus()
+ }
+ updateTitle()
+ let nameObservationToken = tunnel.observe(\TunnelContainer.name) { [weak self] _, _ in
+ self?.updateTitle()
+ }
+ self.statusObservationToken = statusObservationToken
+ self.nameObservationToken = nameObservationToken
+ }
+
+ required init(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func updateTitle() {
+ title = tunnel.name
+ }
+
+ func updateStatus() {
+ let shouldShowCheckmark = (tunnel.status != .inactive && tunnel.status != .deactivating)
+ state = shouldShowCheckmark ? .on : .off
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/TunnelsTracker.swift b/Sources/WireGuardApp/UI/macOS/TunnelsTracker.swift
new file mode 100644
index 0000000..69cc533
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/TunnelsTracker.swift
@@ -0,0 +1,119 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+// Keeps track of tunnels and informs the following objects of changes in tunnels:
+// - Status menu
+// - Status item controller
+// - Tunnels list view controller in the Manage Tunnels window
+
+class TunnelsTracker {
+
+ weak var statusMenu: StatusMenu? {
+ didSet {
+ statusMenu?.currentTunnel = currentTunnel
+ }
+ }
+ weak var statusItemController: StatusItemController? {
+ didSet {
+ statusItemController?.currentTunnel = currentTunnel
+ }
+ }
+ weak var manageTunnelsRootVC: ManageTunnelsRootViewController?
+
+ private var tunnelsManager: TunnelsManager
+ private var tunnelStatusObservers = [AnyObject]()
+ private(set) var currentTunnel: TunnelContainer? {
+ didSet {
+ statusMenu?.currentTunnel = currentTunnel
+ statusItemController?.currentTunnel = currentTunnel
+ }
+ }
+
+ init(tunnelsManager: TunnelsManager) {
+ self.tunnelsManager = tunnelsManager
+ currentTunnel = tunnelsManager.tunnelInOperation()
+
+ for index in 0 ..< tunnelsManager.numberOfTunnels() {
+ let tunnel = tunnelsManager.tunnel(at: index)
+ let statusObservationToken = observeStatus(of: tunnel)
+ tunnelStatusObservers.insert(statusObservationToken, at: index)
+ }
+
+ tunnelsManager.tunnelsListDelegate = self
+ tunnelsManager.activationDelegate = self
+ }
+
+ func observeStatus(of tunnel: TunnelContainer) -> AnyObject {
+ return tunnel.observe(\.status) { [weak self] tunnel, _ in
+ guard let self = self else { return }
+ if tunnel.status == .deactivating || tunnel.status == .inactive {
+ if self.currentTunnel == tunnel {
+ self.currentTunnel = self.tunnelsManager.tunnelInOperation()
+ }
+ } else {
+ self.currentTunnel = tunnel
+ }
+ }
+ }
+}
+
+extension TunnelsTracker: TunnelsManagerListDelegate {
+ func tunnelAdded(at index: Int) {
+ let tunnel = tunnelsManager.tunnel(at: index)
+ if tunnel.status != .deactivating && tunnel.status != .inactive {
+ self.currentTunnel = tunnel
+ }
+ let statusObservationToken = observeStatus(of: tunnel)
+ tunnelStatusObservers.insert(statusObservationToken, at: index)
+
+ statusMenu?.insertTunnelMenuItem(for: tunnel, at: index)
+ manageTunnelsRootVC?.tunnelsListVC?.tunnelAdded(at: index)
+ }
+
+ func tunnelModified(at index: Int) {
+ manageTunnelsRootVC?.tunnelsListVC?.tunnelModified(at: index)
+ }
+
+ func tunnelMoved(from oldIndex: Int, to newIndex: Int) {
+ let statusObserver = tunnelStatusObservers.remove(at: oldIndex)
+ tunnelStatusObservers.insert(statusObserver, at: newIndex)
+
+ statusMenu?.moveTunnelMenuItem(from: oldIndex, to: newIndex)
+ manageTunnelsRootVC?.tunnelsListVC?.tunnelMoved(from: oldIndex, to: newIndex)
+ }
+
+ func tunnelRemoved(at index: Int, tunnel: TunnelContainer) {
+ tunnelStatusObservers.remove(at: index)
+
+ statusMenu?.removeTunnelMenuItem(at: index)
+ manageTunnelsRootVC?.tunnelsListVC?.tunnelRemoved(at: index)
+ }
+}
+
+extension TunnelsTracker: TunnelsManagerActivationDelegate {
+ func tunnelActivationAttemptFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationAttemptError) {
+ if let manageTunnelsRootVC = manageTunnelsRootVC, manageTunnelsRootVC.view.window?.isVisible ?? false {
+ ErrorPresenter.showErrorAlert(error: error, from: manageTunnelsRootVC)
+ } else {
+ ErrorPresenter.showErrorAlert(error: error, from: nil)
+ }
+ }
+
+ func tunnelActivationAttemptSucceeded(tunnel: TunnelContainer) {
+ // Nothing to do
+ }
+
+ func tunnelActivationFailed(tunnel: TunnelContainer, error: TunnelsManagerActivationError) {
+ if let manageTunnelsRootVC = manageTunnelsRootVC, manageTunnelsRootVC.view.window?.isVisible ?? false {
+ ErrorPresenter.showErrorAlert(error: error, from: manageTunnelsRootVC)
+ } else {
+ ErrorPresenter.showErrorAlert(error: error, from: nil)
+ }
+ }
+
+ func tunnelActivationSucceeded(tunnel: TunnelContainer) {
+ // Nothing to do
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/ButtonRow.swift b/Sources/WireGuardApp/UI/macOS/View/ButtonRow.swift
new file mode 100644
index 0000000..4d15f5e
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/ButtonRow.swift
@@ -0,0 +1,67 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class ButtonRow: NSView {
+ let button: NSButton = {
+ let button = NSButton()
+ button.title = ""
+ button.setButtonType(.momentaryPushIn)
+ button.bezelStyle = .rounded
+ return button
+ }()
+
+ var buttonTitle: String {
+ get { return button.title }
+ set(value) { button.title = value }
+ }
+
+ var isButtonEnabled: Bool {
+ get { return button.isEnabled }
+ set(value) { button.isEnabled = value }
+ }
+
+ var buttonToolTip: String {
+ get { return button.toolTip ?? "" }
+ set(value) { button.toolTip = value }
+ }
+
+ var onButtonClicked: (() -> Void)?
+ var observationToken: AnyObject?
+
+ override var intrinsicContentSize: NSSize {
+ return NSSize(width: NSView.noIntrinsicMetric, height: button.intrinsicContentSize.height)
+ }
+
+ init() {
+ super.init(frame: CGRect.zero)
+
+ button.target = self
+ button.action = #selector(buttonClicked)
+
+ addSubview(button)
+ button.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ button.centerYAnchor.constraint(equalTo: self.centerYAnchor),
+ button.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 155),
+ button.widthAnchor.constraint(greaterThanOrEqualToConstant: 100)
+ ])
+ }
+
+ required init?(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ @objc func buttonClicked() {
+ onButtonClicked?()
+ }
+
+ override func prepareForReuse() {
+ buttonTitle = ""
+ buttonToolTip = ""
+ onButtonClicked = nil
+ observationToken = nil
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/ConfTextColorTheme.swift b/Sources/WireGuardApp/UI/macOS/View/ConfTextColorTheme.swift
new file mode 100644
index 0000000..5229d50
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/ConfTextColorTheme.swift
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+protocol ConfTextColorTheme {
+ static var defaultColor: NSColor { get }
+ static var colorMap: [UInt32: NSColor] { get }
+}
+
+struct ConfTextAquaColorTheme: ConfTextColorTheme {
+ static let defaultColor = NSColor(hex: "#000000")
+ static let colorMap: [UInt32: NSColor] = [
+ HighlightSection.rawValue: NSColor(hex: "#326D74"), // Class name in Xcode
+ HighlightField.rawValue: NSColor(hex: "#9B2393"), // Keywords in Xcode
+ HighlightPublicKey.rawValue: NSColor(hex: "#643820"), // Preprocessor directives in Xcode
+ HighlightPrivateKey.rawValue: NSColor(hex: "#643820"), // Preprocessor directives in Xcode
+ HighlightPresharedKey.rawValue: NSColor(hex: "#643820"), // Preprocessor directives in Xcode
+ HighlightIP.rawValue: NSColor(hex: "#0E0EFF"), // URLs in Xcode
+ HighlightHost.rawValue: NSColor(hex: "#0E0EFF"), // URLs in Xcode
+ HighlightCidr.rawValue: NSColor(hex: "#815F03"), // Attributes in Xcode
+ HighlightPort.rawValue: NSColor(hex: "#815F03"), // Attributes in Xcode
+ HighlightMTU.rawValue: NSColor(hex: "#1C00CF"), // Numbers in Xcode
+ HighlightKeepalive.rawValue: NSColor(hex: "#1C00CF"), // Numbers in Xcode
+ HighlightComment.rawValue: NSColor(hex: "#536579"), // Comments in Xcode
+ HighlightError.rawValue: NSColor(hex: "#C41A16") // Strings in Xcode
+ ]
+}
+
+struct ConfTextDarkAquaColorTheme: ConfTextColorTheme {
+ static let defaultColor = NSColor(hex: "#FFFFFF") // Plain text in Xcode
+ static let colorMap: [UInt32: NSColor] = [
+ HighlightSection.rawValue: NSColor(hex: "#91D462"), // Class name in Xcode
+ HighlightField.rawValue: NSColor(hex: "#FC5FA3"), // Keywords in Xcode
+ HighlightPublicKey.rawValue: NSColor(hex: "#FD8F3F"), // Preprocessor directives in Xcode
+ HighlightPrivateKey.rawValue: NSColor(hex: "#FD8F3F"), // Preprocessor directives in Xcode
+ HighlightPresharedKey.rawValue: NSColor(hex: "#FD8F3F"), // Preprocessor directives in Xcode
+ HighlightIP.rawValue: NSColor(hex: "#53A5FB"), // URLs in Xcode
+ HighlightHost.rawValue: NSColor(hex: "#53A5FB"), // URLs in Xcode
+ HighlightCidr.rawValue: NSColor(hex: "#75B492"), // Attributes in Xcode
+ HighlightPort.rawValue: NSColor(hex: "#75B492"), // Attributes in Xcode
+ HighlightMTU.rawValue: NSColor(hex: "#9686F5"), // Numbers in Xcode
+ HighlightKeepalive.rawValue: NSColor(hex: "#9686F5"), // Numbers in Xcode
+ HighlightComment.rawValue: NSColor(hex: "#6C7986"), // Comments in Xcode
+ HighlightError.rawValue: NSColor(hex: "#FF4C4C") // Strings in Xcode
+ ]
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/ConfTextStorage.swift b/Sources/WireGuardApp/UI/macOS/View/ConfTextStorage.swift
new file mode 100644
index 0000000..3c92db3
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/ConfTextStorage.swift
@@ -0,0 +1,180 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+private let fontSize: CGFloat = 15
+
+class ConfTextStorage: NSTextStorage {
+ let defaultFont = NSFontManager.shared.convertWeight(true, of: NSFont.systemFont(ofSize: fontSize))
+ private let boldFont = NSFont.boldSystemFont(ofSize: fontSize)
+ private lazy var italicFont = NSFontManager.shared.convert(defaultFont, toHaveTrait: .italicFontMask)
+
+ private var textColorTheme: ConfTextColorTheme.Type?
+
+ private let backingStore: NSMutableAttributedString
+ private(set) var hasError = false
+ private(set) var privateKeyString: String?
+
+ private(set) var hasOnePeer: Bool = false
+ private(set) var lastOnePeerAllowedIPs = [String]()
+ private(set) var lastOnePeerDNSServers = [String]()
+ private(set) var lastOnePeerHasPublicKey = false
+
+ override init() {
+ backingStore = NSMutableAttributedString(string: "")
+ super.init()
+ }
+
+ required init?(coder aDecoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ required init?(pasteboardPropertyList propertyList: Any, ofType type: NSPasteboard.PasteboardType) {
+ fatalError("init(pasteboardPropertyList:ofType:) has not been implemented")
+ }
+
+ func nonColorAttributes(for highlightType: highlight_type) -> [NSAttributedString.Key: Any] {
+ switch highlightType.rawValue {
+ case HighlightSection.rawValue, HighlightField.rawValue:
+ return [.font: boldFont]
+ case HighlightPublicKey.rawValue, HighlightPrivateKey.rawValue, HighlightPresharedKey.rawValue,
+ HighlightIP.rawValue, HighlightCidr.rawValue, HighlightHost.rawValue, HighlightPort.rawValue,
+ HighlightMTU.rawValue, HighlightKeepalive.rawValue, HighlightDelimiter.rawValue:
+ return [.font: defaultFont]
+ case HighlightComment.rawValue:
+ return [.font: italicFont]
+ case HighlightError.rawValue:
+ return [.font: defaultFont, .underlineStyle: 1]
+ default:
+ return [:]
+ }
+ }
+
+ func updateAttributes(for textColorTheme: ConfTextColorTheme.Type) {
+ self.textColorTheme = textColorTheme
+ highlightSyntax()
+ }
+
+ override var string: String {
+ return backingStore.string
+ }
+
+ override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] {
+ return backingStore.attributes(at: location, effectiveRange: range)
+ }
+
+ override func replaceCharacters(in range: NSRange, with str: String) {
+ beginEditing()
+ backingStore.replaceCharacters(in: range, with: str)
+ edited(.editedCharacters, range: range, changeInLength: str.count - range.length)
+ endEditing()
+ }
+
+ override func replaceCharacters(in range: NSRange, with attrString: NSAttributedString) {
+ beginEditing()
+ backingStore.replaceCharacters(in: range, with: attrString)
+ edited(.editedCharacters, range: range, changeInLength: attrString.length - range.length)
+ endEditing()
+ }
+
+ override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
+ beginEditing()
+ backingStore.setAttributes(attrs, range: range)
+ edited(.editedAttributes, range: range, changeInLength: 0)
+ endEditing()
+ }
+
+ func resetLastPeer() {
+ hasOnePeer = false
+ lastOnePeerAllowedIPs = []
+ lastOnePeerDNSServers = []
+ lastOnePeerHasPublicKey = false
+ }
+
+ func evaluateExcludePrivateIPs(highlightSpans: UnsafePointer<highlight_span>) {
+ var spans = highlightSpans
+ enum FieldType: String {
+ case dns
+ case allowedips
+ }
+ var fieldType: FieldType?
+ resetLastPeer()
+ while spans.pointee.type != HighlightEnd {
+ let span = spans.pointee
+ var substring = backingStore.attributedSubstring(from: NSRange(location: span.start, length: span.len)).string.lowercased()
+
+ if span.type == HighlightError {
+ resetLastPeer()
+ return
+ } else if span.type == HighlightSection {
+ if substring == "[peer]" {
+ if hasOnePeer {
+ resetLastPeer()
+ return
+ }
+ hasOnePeer = true
+ }
+ } else if span.type == HighlightField {
+ fieldType = FieldType(rawValue: substring)
+ } else if span.type == HighlightIP && fieldType == .dns {
+ lastOnePeerDNSServers.append(substring)
+ } else if span.type == HighlightIP && fieldType == .allowedips {
+ let next = spans.successor()
+ let nextnext = next.successor()
+ if next.pointee.type == HighlightDelimiter && nextnext.pointee.type == HighlightCidr {
+ substring += backingStore.attributedSubstring(from: NSRange(location: next.pointee.start, length: next.pointee.len)).string +
+ backingStore.attributedSubstring(from: NSRange(location: nextnext.pointee.start, length: nextnext.pointee.len)).string
+ }
+ lastOnePeerAllowedIPs.append(substring)
+ } else if span.type == HighlightPublicKey {
+ lastOnePeerHasPublicKey = true
+ }
+ spans = spans.successor()
+ }
+ }
+
+ func highlightSyntax() {
+ guard let textColorTheme = textColorTheme else { return }
+ hasError = false
+ privateKeyString = nil
+
+ let fullTextRange = NSRange(location: 0, length: (backingStore.string as NSString).length)
+
+ backingStore.beginEditing()
+ let defaultAttributes: [NSAttributedString.Key: Any] = [
+ .foregroundColor: textColorTheme.defaultColor,
+ .font: defaultFont
+ ]
+ backingStore.setAttributes(defaultAttributes, range: fullTextRange)
+ var spans = highlight_config(backingStore.string)!
+ evaluateExcludePrivateIPs(highlightSpans: spans)
+
+ let spansStart = spans
+ while spans.pointee.type != HighlightEnd {
+ let span = spans.pointee
+
+ let range = NSRange(location: span.start, length: span.len)
+ backingStore.setAttributes(nonColorAttributes(for: span.type), range: range)
+ let color = textColorTheme.colorMap[span.type.rawValue, default: textColorTheme.defaultColor]
+ backingStore.addAttribute(.foregroundColor, value: color, range: range)
+
+ if span.type == HighlightError {
+ hasError = true
+ }
+
+ if span.type == HighlightPrivateKey {
+ privateKeyString = backingStore.attributedSubstring(from: NSRange(location: span.start, length: span.len)).string
+ }
+
+ spans = spans.successor()
+ }
+ backingStore.endEditing()
+ free(spansStart)
+
+ beginEditing()
+ edited(.editedAttributes, range: fullTextRange, changeInLength: 0)
+ endEditing()
+ }
+
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/ConfTextView.swift b/Sources/WireGuardApp/UI/macOS/View/ConfTextView.swift
new file mode 100644
index 0000000..6016e08
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/ConfTextView.swift
@@ -0,0 +1,89 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class ConfTextView: NSTextView {
+
+ private let confTextStorage = ConfTextStorage()
+
+ @objc dynamic var hasError: Bool = false
+ @objc dynamic var privateKeyString: String?
+ @objc dynamic var singlePeerAllowedIPs: [String]?
+
+ override var string: String {
+ didSet {
+ confTextStorage.highlightSyntax()
+ updateConfigData()
+ }
+ }
+
+ init() {
+ let textContainer = NSTextContainer()
+ let layoutManager = NSLayoutManager()
+ layoutManager.addTextContainer(textContainer)
+ confTextStorage.addLayoutManager(layoutManager)
+ super.init(frame: CGRect(x: 0, y: 0, width: 1, height: 60), textContainer: textContainer)
+ font = confTextStorage.defaultFont
+ allowsUndo = true
+ isAutomaticSpellingCorrectionEnabled = false
+ isAutomaticDataDetectionEnabled = false
+ isAutomaticLinkDetectionEnabled = false
+ isAutomaticTextCompletionEnabled = false
+ isAutomaticTextReplacementEnabled = false
+ isAutomaticDashSubstitutionEnabled = false
+ isAutomaticQuoteSubstitutionEnabled = false
+ updateTheme()
+ delegate = self
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func viewDidChangeEffectiveAppearance() {
+ updateTheme()
+ }
+
+ private func updateTheme() {
+ switch effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) ?? .aqua {
+ case .darkAqua:
+ confTextStorage.updateAttributes(for: ConfTextDarkAquaColorTheme.self)
+ default:
+ confTextStorage.updateAttributes(for: ConfTextAquaColorTheme.self)
+ }
+ }
+
+ private func updateConfigData() {
+ if hasError != confTextStorage.hasError {
+ hasError = confTextStorage.hasError
+ }
+ if privateKeyString != confTextStorage.privateKeyString {
+ privateKeyString = confTextStorage.privateKeyString
+ }
+ let hasSyntaxError = confTextStorage.hasError
+ let hasSemanticError = confTextStorage.privateKeyString == nil || !confTextStorage.lastOnePeerHasPublicKey
+ let updatedSinglePeerAllowedIPs = confTextStorage.hasOnePeer && !hasSyntaxError && !hasSemanticError ? confTextStorage.lastOnePeerAllowedIPs : nil
+ if singlePeerAllowedIPs != updatedSinglePeerAllowedIPs {
+ singlePeerAllowedIPs = updatedSinglePeerAllowedIPs
+ }
+ }
+
+ func setConfText(_ text: String) {
+ let fullTextRange = NSRange(location: 0, length: (string as NSString).length)
+ if shouldChangeText(in: fullTextRange, replacementString: text) {
+ replaceCharacters(in: fullTextRange, with: text)
+ didChangeText()
+ }
+ }
+}
+
+extension ConfTextView: NSTextViewDelegate {
+
+ func textDidChange(_ notification: Notification) {
+ confTextStorage.highlightSyntax()
+ updateConfigData()
+ needsDisplay = true
+ }
+
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/DeleteTunnelsConfirmationAlert.swift b/Sources/WireGuardApp/UI/macOS/View/DeleteTunnelsConfirmationAlert.swift
new file mode 100644
index 0000000..e52ed89
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/DeleteTunnelsConfirmationAlert.swift
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class DeleteTunnelsConfirmationAlert: NSAlert {
+ var alertDeleteButton: NSButton?
+ var alertCancelButton: NSButton?
+
+ var onDeleteClicked: ((_ completionHandler: @escaping () -> Void) -> Void)?
+
+ override init() {
+ super.init()
+ let alertDeleteButton = addButton(withTitle: tr("macDeleteTunnelConfirmationAlertButtonTitleDelete"))
+ alertDeleteButton.target = self
+ alertDeleteButton.action = #selector(removeTunnelAlertDeleteClicked)
+ self.alertDeleteButton = alertDeleteButton
+ self.alertCancelButton = addButton(withTitle: tr("macDeleteTunnelConfirmationAlertButtonTitleCancel"))
+ }
+
+ @objc func removeTunnelAlertDeleteClicked() {
+ alertDeleteButton?.title = tr("macDeleteTunnelConfirmationAlertButtonTitleDeleting")
+ alertDeleteButton?.isEnabled = false
+ alertCancelButton?.isEnabled = false
+ if let onDeleteClicked = onDeleteClicked {
+ onDeleteClicked { [weak self] in
+ guard let self = self else { return }
+ self.window.sheetParent?.endSheet(self.window)
+ }
+ }
+ }
+
+ func beginSheetModal(for sheetWindow: NSWindow) {
+ beginSheetModal(for: sheetWindow) { _ in }
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/KeyValueRow.swift b/Sources/WireGuardApp/UI/macOS/View/KeyValueRow.swift
new file mode 100644
index 0000000..2f037d8
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/KeyValueRow.swift
@@ -0,0 +1,139 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class EditableKeyValueRow: NSView {
+ let keyLabel: NSTextField = {
+ let keyLabel = NSTextField()
+ keyLabel.isEditable = false
+ keyLabel.isSelectable = false
+ keyLabel.isBordered = false
+ keyLabel.alignment = .right
+ keyLabel.maximumNumberOfLines = 1
+ keyLabel.lineBreakMode = .byTruncatingTail
+ keyLabel.backgroundColor = .clear
+ return keyLabel
+ }()
+
+ let valueLabel: NSTextField = {
+ let valueLabel = NSTextField()
+ valueLabel.isSelectable = true
+ valueLabel.maximumNumberOfLines = 1
+ valueLabel.lineBreakMode = .byTruncatingTail
+ return valueLabel
+ }()
+
+ let valueImageView: NSImageView?
+
+ var key: String {
+ get { return keyLabel.stringValue }
+ set(value) { keyLabel.stringValue = value }
+ }
+ var value: String {
+ get { return valueLabel.stringValue }
+ set(value) { valueLabel.stringValue = value }
+ }
+ var isKeyInBold: Bool {
+ get { return keyLabel.font == NSFont.boldSystemFont(ofSize: 0) }
+ set(value) {
+ if value {
+ keyLabel.font = NSFont.boldSystemFont(ofSize: 0)
+ } else {
+ keyLabel.font = NSFont.systemFont(ofSize: 0)
+ }
+ }
+ }
+ var valueImage: NSImage? {
+ get { return valueImageView?.image }
+ set(value) { valueImageView?.image = value }
+ }
+
+ var observationToken: AnyObject?
+
+ override var intrinsicContentSize: NSSize {
+ let height = max(keyLabel.intrinsicContentSize.height, valueLabel.intrinsicContentSize.height)
+ return NSSize(width: NSView.noIntrinsicMetric, height: height)
+ }
+
+ convenience init() {
+ self.init(hasValueImage: false)
+ }
+
+ fileprivate init(hasValueImage: Bool) {
+ valueImageView = hasValueImage ? NSImageView() : nil
+ super.init(frame: CGRect.zero)
+
+ addSubview(keyLabel)
+ addSubview(valueLabel)
+ keyLabel.translatesAutoresizingMaskIntoConstraints = false
+ valueLabel.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ keyLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor),
+ keyLabel.firstBaselineAnchor.constraint(equalTo: valueLabel.firstBaselineAnchor),
+ self.leadingAnchor.constraint(equalTo: keyLabel.leadingAnchor),
+ valueLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor)
+ ])
+
+ let spacing: CGFloat = 5
+ if let valueImageView = valueImageView {
+ addSubview(valueImageView)
+ valueImageView.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ valueImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
+ valueImageView.leadingAnchor.constraint(equalTo: keyLabel.trailingAnchor, constant: spacing),
+ valueLabel.leadingAnchor.constraint(equalTo: valueImageView.trailingAnchor)
+ ])
+ } else {
+ NSLayoutConstraint.activate([
+ valueLabel.leadingAnchor.constraint(equalTo: keyLabel.trailingAnchor, constant: spacing)
+ ])
+ }
+
+ keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
+ keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+ valueLabel.setContentHuggingPriority(.defaultLow, for: .horizontal)
+
+ let widthConstraint = keyLabel.widthAnchor.constraint(equalToConstant: 150)
+ widthConstraint.priority = .defaultHigh + 1
+ widthConstraint.isActive = true
+ }
+
+ required init?(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ key = ""
+ value = ""
+ isKeyInBold = false
+ observationToken = nil
+ }
+}
+
+class KeyValueRow: EditableKeyValueRow {
+ init() {
+ super.init(hasValueImage: false)
+ valueLabel.isEditable = false
+ valueLabel.isBordered = false
+ valueLabel.backgroundColor = .clear
+ }
+
+ required init?(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
+
+class KeyValueImageRow: EditableKeyValueRow {
+ init() {
+ super.init(hasValueImage: true)
+ valueLabel.isEditable = false
+ valueLabel.isBordered = false
+ valueLabel.backgroundColor = .clear
+ }
+
+ required init?(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/LogViewCell.swift b/Sources/WireGuardApp/UI/macOS/View/LogViewCell.swift
new file mode 100644
index 0000000..c1c6cc5
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/LogViewCell.swift
@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class LogViewCell: NSTableCellView {
+ var text: String = "" {
+ didSet { textField?.stringValue = text }
+ }
+
+ init() {
+ super.init(frame: .zero)
+
+ let textField = NSTextField(wrappingLabelWithString: "")
+ addSubview(textField)
+ textField.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ textField.leadingAnchor.constraint(equalTo: self.leadingAnchor),
+ textField.trailingAnchor.constraint(equalTo: self.trailingAnchor),
+ textField.topAnchor.constraint(equalTo: self.topAnchor),
+ textField.bottomAnchor.constraint(equalTo: self.bottomAnchor)
+ ])
+
+ self.textField = textField
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func prepareForReuse() {
+ textField?.stringValue = ""
+ }
+}
+
+class LogViewTimestampCell: LogViewCell {
+ override init() {
+ super.init()
+ if let textField = textField {
+ textField.maximumNumberOfLines = 1
+ textField.lineBreakMode = .byClipping
+ textField.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
+ textField.setContentHuggingPriority(.defaultLow, for: .vertical)
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
+
+class LogViewMessageCell: LogViewCell {
+ override init() {
+ super.init()
+ if let textField = textField {
+ textField.maximumNumberOfLines = 0
+ textField.lineBreakMode = .byWordWrapping
+ textField.setContentCompressionResistancePriority(.required, for: .vertical)
+ textField.setContentHuggingPriority(.required, for: .vertical)
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/OnDemandWiFiControls.swift b/Sources/WireGuardApp/UI/macOS/View/OnDemandWiFiControls.swift
new file mode 100644
index 0000000..2e3de48
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/OnDemandWiFiControls.swift
@@ -0,0 +1,171 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+import CoreWLAN
+
+class OnDemandControlsRow: NSView {
+ let keyLabel: NSTextField = {
+ let keyLabel = NSTextField()
+ keyLabel.stringValue = tr("macFieldOnDemand")
+ keyLabel.isEditable = false
+ keyLabel.isSelectable = false
+ keyLabel.isBordered = false
+ keyLabel.alignment = .right
+ keyLabel.maximumNumberOfLines = 1
+ keyLabel.lineBreakMode = .byTruncatingTail
+ keyLabel.backgroundColor = .clear
+ return keyLabel
+ }()
+
+ let onDemandEthernetCheckbox: NSButton = {
+ let checkbox = NSButton()
+ checkbox.title = tr("tunnelOnDemandEthernet")
+ checkbox.setButtonType(.switch)
+ checkbox.state = .off
+ return checkbox
+ }()
+
+ let onDemandWiFiCheckbox: NSButton = {
+ let checkbox = NSButton()
+ checkbox.title = tr("tunnelOnDemandWiFi")
+ checkbox.setButtonType(.switch)
+ checkbox.state = .off
+ return checkbox
+ }()
+
+ static let onDemandSSIDOptions: [ActivateOnDemandViewModel.OnDemandSSIDOption] = [
+ .anySSID, .onlySpecificSSIDs, .exceptSpecificSSIDs
+ ]
+
+ let onDemandSSIDOptionsPopup = NSPopUpButton()
+
+ let onDemandSSIDsField: NSTokenField = {
+ let tokenField = NSTokenField()
+ tokenField.tokenizingCharacterSet = CharacterSet([])
+ tokenField.tokenStyle = .squared
+ NSLayoutConstraint.activate([
+ tokenField.widthAnchor.constraint(greaterThanOrEqualToConstant: 180)
+ ])
+ return tokenField
+ }()
+
+ override var intrinsicContentSize: NSSize {
+ let minHeight: CGFloat = 22
+ let height = max(minHeight, keyLabel.intrinsicContentSize.height,
+ onDemandEthernetCheckbox.intrinsicContentSize.height, onDemandWiFiCheckbox.intrinsicContentSize.height,
+ onDemandSSIDOptionsPopup.intrinsicContentSize.height, onDemandSSIDsField.intrinsicContentSize.height)
+ return NSSize(width: NSView.noIntrinsicMetric, height: height)
+ }
+
+ var onDemandViewModel: ActivateOnDemandViewModel? {
+ didSet { updateControls() }
+ }
+
+ var currentSSIDs: [String]
+
+ init() {
+ currentSSIDs = getCurrentSSIDs()
+ super.init(frame: CGRect.zero)
+
+ onDemandSSIDOptionsPopup.addItems(withTitles: OnDemandControlsRow.onDemandSSIDOptions.map { $0.localizedUIString })
+
+ let stackView = NSStackView()
+ stackView.setViews([onDemandEthernetCheckbox, onDemandWiFiCheckbox, onDemandSSIDOptionsPopup, onDemandSSIDsField], in: .leading)
+ stackView.orientation = .horizontal
+
+ addSubview(keyLabel)
+ addSubview(stackView)
+ keyLabel.translatesAutoresizingMaskIntoConstraints = false
+ stackView.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ keyLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor),
+ stackView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
+ self.leadingAnchor.constraint(equalTo: keyLabel.leadingAnchor),
+ stackView.leadingAnchor.constraint(equalTo: keyLabel.trailingAnchor, constant: 5),
+ stackView.trailingAnchor.constraint(equalTo: self.trailingAnchor)
+ ])
+
+ keyLabel.setContentCompressionResistancePriority(.defaultHigh + 2, for: .horizontal)
+ keyLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
+
+ let widthConstraint = keyLabel.widthAnchor.constraint(equalToConstant: 150)
+ widthConstraint.priority = .defaultHigh + 1
+ widthConstraint.isActive = true
+
+ NSLayoutConstraint.activate([
+ onDemandEthernetCheckbox.centerYAnchor.constraint(equalTo: stackView.centerYAnchor),
+ onDemandWiFiCheckbox.lastBaselineAnchor.constraint(equalTo: onDemandEthernetCheckbox.lastBaselineAnchor),
+ onDemandSSIDOptionsPopup.lastBaselineAnchor.constraint(equalTo: onDemandEthernetCheckbox.lastBaselineAnchor),
+ onDemandSSIDsField.lastBaselineAnchor.constraint(equalTo: onDemandEthernetCheckbox.lastBaselineAnchor)
+ ])
+
+ onDemandSSIDsField.setContentHuggingPriority(.defaultLow, for: .horizontal)
+
+ onDemandEthernetCheckbox.target = self
+ onDemandEthernetCheckbox.action = #selector(ethernetCheckboxToggled)
+
+ onDemandWiFiCheckbox.target = self
+ onDemandWiFiCheckbox.action = #selector(wiFiCheckboxToggled)
+
+ onDemandSSIDOptionsPopup.target = self
+ onDemandSSIDOptionsPopup.action = #selector(ssidOptionsPopupValueChanged)
+
+ onDemandSSIDsField.delegate = self
+
+ updateControls()
+ }
+
+ required init?(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func saveToViewModel() {
+ guard let onDemandViewModel = onDemandViewModel else { return }
+ onDemandViewModel.isNonWiFiInterfaceEnabled = onDemandEthernetCheckbox.state == .on
+ onDemandViewModel.isWiFiInterfaceEnabled = onDemandWiFiCheckbox.state == .on
+ onDemandViewModel.ssidOption = OnDemandControlsRow.onDemandSSIDOptions[onDemandSSIDOptionsPopup.indexOfSelectedItem]
+ onDemandViewModel.selectedSSIDs = (onDemandSSIDsField.objectValue as? [String]) ?? []
+ }
+
+ func updateControls() {
+ guard let onDemandViewModel = onDemandViewModel else { return }
+ onDemandEthernetCheckbox.state = onDemandViewModel.isNonWiFiInterfaceEnabled ? .on : .off
+ onDemandWiFiCheckbox.state = onDemandViewModel.isWiFiInterfaceEnabled ? .on : .off
+ let optionIndex = OnDemandControlsRow.onDemandSSIDOptions.firstIndex(of: onDemandViewModel.ssidOption)
+ onDemandSSIDOptionsPopup.selectItem(at: optionIndex ?? 0)
+ onDemandSSIDsField.objectValue = onDemandViewModel.selectedSSIDs
+ onDemandSSIDOptionsPopup.isHidden = !onDemandViewModel.isWiFiInterfaceEnabled
+ onDemandSSIDsField.isHidden = !onDemandViewModel.isWiFiInterfaceEnabled || onDemandViewModel.ssidOption == .anySSID
+ }
+
+ @objc func ethernetCheckboxToggled() {
+ onDemandViewModel?.isNonWiFiInterfaceEnabled = onDemandEthernetCheckbox.state == .on
+ }
+
+ @objc func wiFiCheckboxToggled() {
+ onDemandViewModel?.isWiFiInterfaceEnabled = onDemandWiFiCheckbox.state == .on
+ updateControls()
+ }
+
+ @objc func ssidOptionsPopupValueChanged() {
+ let selectedIndex = onDemandSSIDOptionsPopup.indexOfSelectedItem
+ onDemandViewModel?.ssidOption = OnDemandControlsRow.onDemandSSIDOptions[selectedIndex]
+ onDemandViewModel?.selectedSSIDs = (onDemandSSIDsField.objectValue as? [String]) ?? []
+ updateControls()
+ if !onDemandSSIDsField.isHidden {
+ onDemandSSIDsField.becomeFirstResponder()
+ }
+ }
+}
+
+extension OnDemandControlsRow: NSTokenFieldDelegate {
+ func tokenField(_ tokenField: NSTokenField, completionsForSubstring substring: String, indexOfToken tokenIndex: Int, indexOfSelectedItem selectedIndex: UnsafeMutablePointer<Int>?) -> [Any]? {
+ return currentSSIDs.filter { $0.hasPrefix(substring) }
+ }
+}
+
+private func getCurrentSSIDs() -> [String] {
+ return CWWiFiClient.shared().interfaces()?.compactMap { $0.ssid() } ?? []
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/TunnelListRow.swift b/Sources/WireGuardApp/UI/macOS/View/TunnelListRow.swift
new file mode 100644
index 0000000..f5e37c1
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/TunnelListRow.swift
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class TunnelListRow: NSView {
+ var tunnel: TunnelContainer? {
+ didSet(value) {
+ // Bind to the tunnel's name
+ nameLabel.stringValue = tunnel?.name ?? ""
+ nameObservationToken = tunnel?.observe(\TunnelContainer.name) { [weak self] tunnel, _ in
+ self?.nameLabel.stringValue = tunnel.name
+ }
+ // Bind to the tunnel's status
+ statusImageView.image = TunnelListRow.image(for: tunnel?.status)
+ statusObservationToken = tunnel?.observe(\TunnelContainer.status) { [weak self] tunnel, _ in
+ self?.statusImageView.image = TunnelListRow.image(for: tunnel.status)
+ }
+ }
+ }
+
+ let nameLabel: NSTextField = {
+ let nameLabel = NSTextField()
+ nameLabel.isEditable = false
+ nameLabel.isSelectable = false
+ nameLabel.isBordered = false
+ nameLabel.maximumNumberOfLines = 1
+ nameLabel.lineBreakMode = .byTruncatingTail
+ return nameLabel
+ }()
+
+ let statusImageView = NSImageView()
+
+ private var statusObservationToken: AnyObject?
+ private var nameObservationToken: AnyObject?
+
+ init() {
+ super.init(frame: CGRect.zero)
+
+ addSubview(statusImageView)
+ addSubview(nameLabel)
+ statusImageView.translatesAutoresizingMaskIntoConstraints = false
+ nameLabel.translatesAutoresizingMaskIntoConstraints = false
+ nameLabel.backgroundColor = .clear
+ NSLayoutConstraint.activate([
+ self.leadingAnchor.constraint(equalTo: statusImageView.leadingAnchor),
+ statusImageView.trailingAnchor.constraint(equalTo: nameLabel.leadingAnchor),
+ statusImageView.widthAnchor.constraint(equalToConstant: 20),
+ nameLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor),
+ statusImageView.centerYAnchor.constraint(equalTo: self.centerYAnchor),
+ nameLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor)
+ ])
+ }
+
+ required init?(coder decoder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ static func image(for status: TunnelStatus?) -> NSImage? {
+ guard let status = status else { return nil }
+ switch status {
+ case .active, .restarting, .reasserting:
+ return NSImage(named: NSImage.statusAvailableName)
+ case .activating, .waiting, .deactivating:
+ return NSImage(named: NSImage.statusPartiallyAvailableName)
+ case .inactive:
+ return NSImage(named: NSImage.statusNoneName)
+ }
+ }
+
+ override func prepareForReuse() {
+ nameLabel.stringValue = ""
+ statusImageView.image = nil
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/highlighter.c b/Sources/WireGuardApp/UI/macOS/View/highlighter.c
new file mode 100644
index 0000000..e0d4e04
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/highlighter.c
@@ -0,0 +1,641 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (C) 2015-2020 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include "highlighter.h"
+
+typedef struct {
+ const char *s;
+ size_t len;
+} string_span_t;
+
+static bool is_decimal(char c)
+{
+ return c >= '0' && c <= '9';
+}
+
+static bool is_hexadecimal(char c)
+{
+ return is_decimal(c) || ((c | 32) >= 'a' && (c | 32) <= 'f');
+}
+
+static bool is_alphabet(char c)
+{
+ return (c | 32) >= 'a' && (c | 32) <= 'z';
+}
+
+static bool is_same(string_span_t s, const char *c)
+{
+ size_t len = strlen(c);
+
+ if (len != s.len)
+ return false;
+ return !memcmp(s.s, c, len);
+}
+
+static bool is_caseless_same(string_span_t s, const char *c)
+{
+ size_t len = strlen(c);
+
+ if (len != s.len)
+ return false;
+ for (size_t i = 0; i < len; ++i) {
+ char a = c[i], b = s.s[i];
+ if ((unsigned)a - 'a' < 26)
+ a &= 95;
+ if ((unsigned)b - 'a' < 26)
+ b &= 95;
+ if (a != b)
+ return false;
+ }
+ return true;
+}
+
+static bool is_valid_key(string_span_t s)
+{
+ if (s.len != 44 || s.s[43] != '=')
+ return false;
+
+ for (size_t i = 0; i < 42; ++i) {
+ if (!is_decimal(s.s[i]) && !is_alphabet(s.s[i]) &&
+ s.s[i] != '/' && s.s[i] != '+')
+ return false;
+ }
+ switch (s.s[42]) {
+ case 'A':
+ case 'E':
+ case 'I':
+ case 'M':
+ case 'Q':
+ case 'U':
+ case 'Y':
+ case 'c':
+ case 'g':
+ case 'k':
+ case 'o':
+ case 's':
+ case 'w':
+ case '4':
+ case '8':
+ case '0':
+ break;
+ default:
+ return false;
+ }
+ return true;
+}
+
+static bool is_valid_hostname(string_span_t s)
+{
+ size_t num_digit = 0, num_entity = s.len;
+
+ if (s.len > 63 || !s.len)
+ return false;
+ if (s.s[0] == '-' || s.s[s.len - 1] == '-')
+ return false;
+ if (s.s[0] == '.' || s.s[s.len - 1] == '.')
+ return false;
+
+ for (size_t i = 0; i < s.len; ++i) {
+ if (is_decimal(s.s[i])) {
+ ++num_digit;
+ continue;
+ }
+ if (s.s[i] == '.') {
+ --num_entity;
+ continue;
+ }
+
+ if (!is_alphabet(s.s[i]) && s.s[i] != '-')
+ return false;
+
+ if (i && s.s[i] == '.' && s.s[i - 1] == '.')
+ return false;
+ }
+ return num_digit != num_entity;
+}
+
+static bool is_valid_ipv4(string_span_t s)
+{
+ for (size_t j, i = 0, pos = 0; i < 4 && pos < s.len; ++i) {
+ uint32_t val = 0;
+
+ for (j = 0; j < 3 && pos + j < s.len && is_decimal(s.s[pos + j]); ++j)
+ val = 10 * val + s.s[pos + j] - '0';
+ if (j == 0 || (j > 1 && s.s[pos] == '0') || val > 255)
+ return false;
+ if (pos + j == s.len && i == 3)
+ return true;
+ if (s.s[pos + j] != '.')
+ return false;
+ pos += j + 1;
+ }
+ return false;
+}
+
+static bool is_valid_ipv6(string_span_t s)
+{
+ size_t pos = 0;
+ bool seen_colon = false;
+
+ if (s.len < 2)
+ return false;
+ if (s.s[pos] == ':' && s.s[++pos] != ':')
+ return false;
+ if (s.s[s.len - 1] == ':' && s.s[s.len - 2] != ':')
+ return false;
+
+ for (size_t j, i = 0; pos < s.len; ++i) {
+ if (s.s[pos] == ':' && !seen_colon) {
+ seen_colon = true;
+ if (++pos == s.len)
+ break;
+ if (i == 7)
+ return false;
+ continue;
+ }
+ for (j = 0; j < 4 && pos + j < s.len && is_hexadecimal(s.s[pos + j]); ++j);
+ if (j == 0)
+ return false;
+ if (pos + j == s.len && (seen_colon || i == 7))
+ break;
+ if (i == 7)
+ return false;
+ if (s.s[pos + j] != ':') {
+ if (s.s[pos + j] != '.' || (i < 6 && !seen_colon))
+ return false;
+ return is_valid_ipv4((string_span_t){ s.s + pos, s.len - pos });
+ }
+ pos += j + 1;
+ }
+ return true;
+}
+
+static bool is_valid_uint(string_span_t s, bool support_hex, uint64_t min, uint64_t max)
+{
+ uint64_t val = 0;
+
+ /* Bound this around 32 bits, so that we don't have to write overflow logic. */
+ if (s.len > 10 || !s.len)
+ return false;
+
+ if (support_hex && s.len > 2 && s.s[0] == '0' && s.s[1] == 'x') {
+ for (size_t i = 2; i < s.len; ++i) {
+ if ((unsigned)s.s[i] - '0' < 10)
+ val = 16 * val + (s.s[i] - '0');
+ else if (((unsigned)s.s[i] | 32) - 'a' < 6)
+ val = 16 * val + (s.s[i] | 32) - 'a' + 10;
+ else
+ return false;
+ }
+ } else {
+ for (size_t i = 0; i < s.len; ++i) {
+ if (!is_decimal(s.s[i]))
+ return false;
+ val = 10 * val + s.s[i] - '0';
+ }
+ }
+ return val <= max && val >= min;
+}
+
+static bool is_valid_port(string_span_t s)
+{
+ return is_valid_uint(s, false, 0, 65535);
+}
+
+static bool is_valid_mtu(string_span_t s)
+{
+ return is_valid_uint(s, false, 576, 65535);
+}
+
+static bool is_valid_persistentkeepalive(string_span_t s)
+{
+ if (is_same(s, "off"))
+ return true;
+ return is_valid_uint(s, false, 0, 65535);
+}
+
+#ifndef MOBILE_WGQUICK_SUBSET
+
+static bool is_valid_fwmark(string_span_t s)
+{
+ if (is_same(s, "off"))
+ return true;
+ return is_valid_uint(s, true, 0, 4294967295);
+}
+
+static bool is_valid_table(string_span_t s)
+{
+ if (is_same(s, "auto"))
+ return true;
+ if (is_same(s, "off"))
+ return true;
+ /* This pretty much invalidates the other checks, but rt_names.c's
+ * fread_id_name does no validation aside from this. */
+ if (s.len < 512)
+ return true;
+ return is_valid_uint(s, false, 0, 4294967295);
+}
+
+static bool is_valid_saveconfig(string_span_t s)
+{
+ return is_same(s, "true") || is_same(s, "false");
+}
+
+static bool is_valid_prepostupdown(string_span_t s)
+{
+ /* It's probably not worthwhile to try to validate a bash expression.
+ * So instead we just demand non-zero length. */
+ return s.len;
+}
+#endif
+
+static bool is_valid_scope(string_span_t s)
+{
+ if (s.len > 64 || !s.len)
+ return false;
+ for (size_t i = 0; i < s.len; ++i) {
+ if (!is_alphabet(s.s[i]) && !is_decimal(s.s[i]) &&
+ s.s[i] != '_' && s.s[i] != '=' && s.s[i] != '+' &&
+ s.s[i] != '.' && s.s[i] != '-')
+ return false;
+ }
+ return true;
+}
+
+static bool is_valid_endpoint(string_span_t s)
+{
+
+ if (!s.len)
+ return false;
+
+ if (s.s[0] == '[') {
+ bool seen_scope = false;
+ string_span_t hostspan = { s.s + 1, 0 };
+
+ for (size_t i = 1; i < s.len; ++i) {
+ if (s.s[i] == '%') {
+ if (seen_scope)
+ return false;
+ seen_scope = true;
+ if (!is_valid_ipv6(hostspan))
+ return false;
+ hostspan = (string_span_t){ s.s + i + 1, 0 };
+ } else if (s.s[i] == ']') {
+ if (seen_scope) {
+ if (!is_valid_scope(hostspan))
+ return false;
+ } else if (!is_valid_ipv6(hostspan)) {
+ return false;
+ }
+ if (i == s.len - 1 || s.s[i + 1] != ':')
+ return false;
+ return is_valid_port((string_span_t){ s.s + i + 2, s.len - i - 2 });
+ } else {
+ ++hostspan.len;
+ }
+ }
+ return false;
+ }
+ for (size_t i = 0; i < s.len; ++i) {
+ if (s.s[i] == ':') {
+ string_span_t host = { s.s, i }, port = { s.s + i + 1, s.len - i - 1};
+ return is_valid_port(port) && (is_valid_ipv4(host) || is_valid_hostname(host));
+ }
+ }
+ return false;
+}
+
+static bool is_valid_network(string_span_t s)
+{
+ for (size_t i = 0; i < s.len; ++i) {
+ if (s.s[i] == '/') {
+ string_span_t ip = { s.s, i }, cidr = { s.s + i + 1, s.len - i - 1};
+ uint16_t cidrval = 0;
+
+ if (cidr.len > 3 || !cidr.len)
+ return false;
+
+ for (size_t j = 0; j < cidr.len; ++j) {
+ if (!is_decimal(cidr.s[j]))
+ return false;
+ cidrval = 10 * cidrval + cidr.s[j] - '0';
+ }
+ if (is_valid_ipv4(ip))
+ return cidrval <= 32;
+ else if (is_valid_ipv6(ip))
+ return cidrval <= 128;
+ return false;
+ }
+ }
+ return is_valid_ipv4(s) || is_valid_ipv6(s);
+}
+
+static bool is_valid_dns(string_span_t s)
+{
+ return is_valid_ipv4(s) || is_valid_ipv6(s);
+}
+
+enum field {
+ InterfaceSection,
+ PrivateKey,
+ ListenPort,
+ Address,
+ DNS,
+ MTU,
+#ifndef MOBILE_WGQUICK_SUBSET
+ FwMark,
+ Table,
+ PreUp, PostUp, PreDown, PostDown,
+ SaveConfig,
+#endif
+
+ PeerSection,
+ PublicKey,
+ PresharedKey,
+ AllowedIPs,
+ Endpoint,
+ PersistentKeepalive,
+
+ Invalid
+};
+
+static enum field section_for_field(enum field t)
+{
+ if (t > InterfaceSection && t < PeerSection)
+ return InterfaceSection;
+ if (t > PeerSection && t < Invalid)
+ return PeerSection;
+ return Invalid;
+}
+
+static enum field get_field(string_span_t s)
+{
+#define check_enum(t) do { if (is_caseless_same(s, #t)) return t; } while (0)
+ check_enum(PrivateKey);
+ check_enum(ListenPort);
+ check_enum(Address);
+ check_enum(DNS);
+ check_enum(MTU);
+ check_enum(PublicKey);
+ check_enum(PresharedKey);
+ check_enum(AllowedIPs);
+ check_enum(Endpoint);
+ check_enum(PersistentKeepalive);
+#ifndef MOBILE_WGQUICK_SUBSET
+ check_enum(FwMark);
+ check_enum(Table);
+ check_enum(PreUp);
+ check_enum(PostUp);
+ check_enum(PreDown);
+ check_enum(PostDown);
+ check_enum(SaveConfig);
+#endif
+ return Invalid;
+#undef check_enum
+}
+
+static enum field get_sectiontype(string_span_t s)
+{
+ if (is_caseless_same(s, "[Peer]"))
+ return PeerSection;
+ if (is_caseless_same(s, "[Interface]"))
+ return InterfaceSection;
+ return Invalid;
+}
+
+struct highlight_span_array {
+ size_t len, capacity;
+ struct highlight_span *spans;
+};
+
+/* A useful OpenBSD-ism. */
+static void *realloc_array(void *optr, size_t nmemb, size_t size)
+{
+ if ((nmemb >= (size_t)1 << (sizeof(size_t) * 4) ||
+ size >= (size_t)1 << (sizeof(size_t) * 4)) &&
+ nmemb > 0 && SIZE_MAX / nmemb < size) {
+ errno = ENOMEM;
+ return NULL;
+ }
+ return realloc(optr, size * nmemb);
+}
+
+static bool append_highlight_span(struct highlight_span_array *a, const char *o, string_span_t s, enum highlight_type t)
+{
+ if (!s.len)
+ return true;
+ if (a->len >= a->capacity) {
+ struct highlight_span *resized;
+
+ a->capacity = a->capacity ? a->capacity * 2 : 64;
+ resized = realloc_array(a->spans, a->capacity, sizeof(*resized));
+ if (!resized) {
+ free(a->spans);
+ memset(a, 0, sizeof(*a));
+ return false;
+ }
+ a->spans = resized;
+ }
+ a->spans[a->len++] = (struct highlight_span){ t, s.s - o, s.len };
+ return true;
+}
+
+static void highlight_multivalue_value(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
+{
+ switch (section) {
+ case DNS:
+ append_highlight_span(ret, parent.s, s, is_valid_dns(s) ? HighlightIP : HighlightError);
+ break;
+ case Address:
+ case AllowedIPs: {
+ size_t slash;
+
+ if (!is_valid_network(s)) {
+ append_highlight_span(ret, parent.s, s, HighlightError);
+ break;
+ }
+ for (slash = 0; slash < s.len; ++slash) {
+ if (s.s[slash] == '/')
+ break;
+ }
+ if (slash == s.len) {
+ append_highlight_span(ret, parent.s, s, HighlightIP);
+ } else {
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s, slash }, HighlightIP);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + slash, 1 }, HighlightDelimiter);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + slash + 1, s.len - slash - 1 }, HighlightCidr);
+ }
+ break;
+ }
+ default:
+ append_highlight_span(ret, parent.s, s, HighlightError);
+ }
+}
+
+static void highlight_multivalue(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
+{
+ string_span_t current_span = { s.s, 0 };
+ size_t len_at_last_space = 0;
+
+ for (size_t i = 0; i < s.len; ++i) {
+ if (s.s[i] == ',') {
+ current_span.len = len_at_last_space;
+ highlight_multivalue_value(ret, parent, current_span, section);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + i, 1 }, HighlightDelimiter);
+ len_at_last_space = 0;
+ current_span = (string_span_t){ s.s + i + 1, 0 };
+ } else if (s.s[i] == ' ' || s.s[i] == '\t') {
+ if (&s.s[i] == current_span.s && !current_span.len)
+ ++current_span.s;
+ else
+ ++current_span.len;
+ } else {
+ len_at_last_space = ++current_span.len;
+ }
+ }
+ current_span.len = len_at_last_space;
+ if (current_span.len)
+ highlight_multivalue_value(ret, parent, current_span, section);
+ else if (ret->spans[ret->len - 1].type == HighlightDelimiter)
+ ret->spans[ret->len - 1].type = HighlightError;
+}
+
+static void highlight_value(struct highlight_span_array *ret, const string_span_t parent, const string_span_t s, enum field section)
+{
+ switch (section) {
+ case PrivateKey:
+ append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPrivateKey : HighlightError);
+ break;
+ case PublicKey:
+ append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPublicKey : HighlightError);
+ break;
+ case PresharedKey:
+ append_highlight_span(ret, parent.s, s, is_valid_key(s) ? HighlightPresharedKey : HighlightError);
+ break;
+ case MTU:
+ append_highlight_span(ret, parent.s, s, is_valid_mtu(s) ? HighlightMTU : HighlightError);
+ break;
+#ifndef MOBILE_WGQUICK_SUBSET
+ case SaveConfig:
+ append_highlight_span(ret, parent.s, s, is_valid_saveconfig(s) ? HighlightSaveConfig : HighlightError);
+ break;
+ case FwMark:
+ append_highlight_span(ret, parent.s, s, is_valid_fwmark(s) ? HighlightFwMark : HighlightError);
+ break;
+ case Table:
+ append_highlight_span(ret, parent.s, s, is_valid_table(s) ? HighlightTable : HighlightError);
+ break;
+ case PreUp:
+ case PostUp:
+ case PreDown:
+ case PostDown:
+ append_highlight_span(ret, parent.s, s, is_valid_prepostupdown(s) ? HighlightCmd : HighlightError);
+ break;
+#endif
+ case ListenPort:
+ append_highlight_span(ret, parent.s, s, is_valid_port(s) ? HighlightPort : HighlightError);
+ break;
+ case PersistentKeepalive:
+ append_highlight_span(ret, parent.s, s, is_valid_persistentkeepalive(s) ? HighlightKeepalive : HighlightError);
+ break;
+ case Endpoint: {
+ size_t colon;
+
+ if (!is_valid_endpoint(s)) {
+ append_highlight_span(ret, parent.s, s, HighlightError);
+ break;
+ }
+ for (colon = s.len; colon --> 0;) {
+ if (s.s[colon] == ':')
+ break;
+ }
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s, colon }, HighlightHost);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + colon, 1 }, HighlightDelimiter);
+ append_highlight_span(ret, parent.s, (string_span_t){ s.s + colon + 1, s.len - colon - 1 }, HighlightPort);
+ break;
+ }
+ case Address:
+ case DNS:
+ case AllowedIPs:
+ highlight_multivalue(ret, parent, s, section);
+ break;
+ default:
+ append_highlight_span(ret, parent.s, s, HighlightError);
+ }
+}
+
+struct highlight_span *highlight_config(const char *config)
+{
+ struct highlight_span_array ret = { 0 };
+ const string_span_t s = { config, strlen(config) };
+ string_span_t current_span = { s.s, 0 };
+ enum field current_section = Invalid, current_field = Invalid;
+ enum { OnNone, OnKey, OnValue, OnComment, OnSection } state = OnNone;
+ size_t len_at_last_space = 0, equals_location = 0;
+
+ for (size_t i = 0; i <= s.len; ++i) {
+ if (i == s.len || s.s[i] == '\n' || (state != OnComment && s.s[i] == '#')) {
+ if (state == OnKey) {
+ current_span.len = len_at_last_space;
+ append_highlight_span(&ret, s.s, current_span, HighlightError);
+ } else if (state == OnValue) {
+ if (current_span.len) {
+ append_highlight_span(&ret, s.s, (string_span_t){ s.s + equals_location, 1 }, HighlightDelimiter);
+ current_span.len = len_at_last_space;
+ highlight_value(&ret, s, current_span, current_field);
+ } else {
+ append_highlight_span(&ret, s.s, (string_span_t){ s.s + equals_location, 1 }, HighlightError);
+ }
+ } else if (state == OnSection) {
+ current_span.len = len_at_last_space;
+ current_section = get_sectiontype(current_span);
+ append_highlight_span(&ret, s.s, current_span, current_section == Invalid ? HighlightError : HighlightSection);
+ } else if (state == OnComment) {
+ append_highlight_span(&ret, s.s, current_span, HighlightComment);
+ }
+ if (i == s.len)
+ break;
+ len_at_last_space = 0;
+ current_field = Invalid;
+ if (s.s[i] == '#') {
+ current_span = (string_span_t){ s.s + i, 1 };
+ state = OnComment;
+ } else {
+ current_span = (string_span_t){ s.s + i + 1, 0 };
+ state = OnNone;
+ }
+ } else if (state == OnComment) {
+ ++current_span.len;
+ } else if (s.s[i] == ' ' || s.s[i] == '\t') {
+ if (&s.s[i] == current_span.s && !current_span.len)
+ ++current_span.s;
+ else
+ ++current_span.len;
+ } else if (s.s[i] == '=' && state == OnKey) {
+ current_span.len = len_at_last_space;
+ current_field = get_field(current_span);
+ enum field section = section_for_field(current_field);
+ if (section == Invalid || current_field == Invalid || section != current_section)
+ append_highlight_span(&ret, s.s, current_span, HighlightError);
+ else
+ append_highlight_span(&ret, s.s, current_span, HighlightField);
+ equals_location = i;
+ current_span = (string_span_t){ s.s + i + 1, 0 };
+ state = OnValue;
+ } else {
+ if (state == OnNone)
+ state = s.s[i] == '[' ? OnSection : OnKey;
+ len_at_last_space = ++current_span.len;
+ }
+ }
+
+ append_highlight_span(&ret, s.s, (string_span_t){ s.s, -1 }, HighlightEnd);
+ return ret.spans;
+}
diff --git a/Sources/WireGuardApp/UI/macOS/View/highlighter.h b/Sources/WireGuardApp/UI/macOS/View/highlighter.h
new file mode 100644
index 0000000..885db2d
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/View/highlighter.h
@@ -0,0 +1,38 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
+ */
+
+#include <sys/types.h>
+#define MOBILE_WGQUICK_SUBSET
+
+enum highlight_type {
+ HighlightSection,
+ HighlightField,
+ HighlightPrivateKey,
+ HighlightPublicKey,
+ HighlightPresharedKey,
+ HighlightIP,
+ HighlightCidr,
+ HighlightHost,
+ HighlightPort,
+ HighlightMTU,
+ HighlightKeepalive,
+ HighlightComment,
+ HighlightDelimiter,
+#ifndef MOBILE_WGQUICK_SUBSET
+ HighlightTable,
+ HighlightFwMark,
+ HighlightSaveConfig,
+ HighlightCmd,
+#endif
+ HighlightError,
+ HighlightEnd
+};
+
+struct highlight_span {
+ enum highlight_type type;
+ size_t start, len;
+};
+
+struct highlight_span *highlight_config(const char *config);
diff --git a/Sources/WireGuardApp/UI/macOS/ViewController/ButtonedDetailViewController.swift b/Sources/WireGuardApp/UI/macOS/ViewController/ButtonedDetailViewController.swift
new file mode 100644
index 0000000..09c2414
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/ViewController/ButtonedDetailViewController.swift
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class ButtonedDetailViewController: NSViewController {
+
+ var onButtonClicked: (() -> Void)?
+
+ let button: NSButton = {
+ let button = NSButton()
+ button.title = ""
+ button.setButtonType(.momentaryPushIn)
+ button.bezelStyle = .rounded
+ return button
+ }()
+
+ init() {
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func loadView() {
+ let view = NSView()
+
+ button.target = self
+ button.action = #selector(buttonClicked)
+
+ view.addSubview(button)
+ button.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
+ ])
+
+ NSLayoutConstraint.activate([
+ view.widthAnchor.constraint(greaterThanOrEqualToConstant: 320),
+ view.heightAnchor.constraint(greaterThanOrEqualToConstant: 120)
+ ])
+
+ self.view = view
+ }
+
+ func setButtonTitle(_ title: String) {
+ button.title = title
+ }
+
+ @objc func buttonClicked() {
+ onButtonClicked?()
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/ViewController/LogViewController.swift b/Sources/WireGuardApp/UI/macOS/ViewController/LogViewController.swift
new file mode 100644
index 0000000..39ce663
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/ViewController/LogViewController.swift
@@ -0,0 +1,274 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class LogViewController: NSViewController {
+
+ enum LogColumn: String {
+ case time = "Time"
+ case logMessage = "LogMessage"
+
+ func createColumn() -> NSTableColumn {
+ return NSTableColumn(identifier: NSUserInterfaceItemIdentifier(rawValue))
+ }
+
+ func isRepresenting(tableColumn: NSTableColumn?) -> Bool {
+ return tableColumn?.identifier.rawValue == rawValue
+ }
+ }
+
+ let scrollView: NSScrollView = {
+ let scrollView = NSScrollView()
+ scrollView.hasVerticalScroller = true
+ scrollView.autohidesScrollers = false
+ scrollView.borderType = .bezelBorder
+ return scrollView
+ }()
+
+ let tableView: NSTableView = {
+ let tableView = NSTableView()
+ let timeColumn = LogColumn.time.createColumn()
+ timeColumn.title = tr("macLogColumnTitleTime")
+ timeColumn.width = 160
+ timeColumn.resizingMask = []
+ tableView.addTableColumn(timeColumn)
+ let messageColumn = LogColumn.logMessage.createColumn()
+ messageColumn.title = tr("macLogColumnTitleLogMessage")
+ messageColumn.minWidth = 360
+ messageColumn.resizingMask = .autoresizingMask
+ tableView.addTableColumn(messageColumn)
+ tableView.rowSizeStyle = .custom
+ tableView.rowHeight = 16
+ tableView.usesAlternatingRowBackgroundColors = true
+ tableView.usesAutomaticRowHeights = true
+ tableView.allowsColumnReordering = false
+ tableView.allowsColumnResizing = true
+ tableView.allowsMultipleSelection = true
+ return tableView
+ }()
+
+ let progressIndicator: NSProgressIndicator = {
+ let progressIndicator = NSProgressIndicator()
+ progressIndicator.controlSize = .small
+ progressIndicator.isIndeterminate = true
+ progressIndicator.style = .spinning
+ progressIndicator.isDisplayedWhenStopped = false
+ return progressIndicator
+ }()
+
+ let closeButton: NSButton = {
+ let button = NSButton()
+ button.title = tr("macLogButtonTitleClose")
+ button.setButtonType(.momentaryPushIn)
+ button.bezelStyle = .rounded
+ return button
+ }()
+
+ let saveButton: NSButton = {
+ let button = NSButton()
+ button.title = tr("macLogButtonTitleSave")
+ button.setButtonType(.momentaryPushIn)
+ button.bezelStyle = .rounded
+ return button
+ }()
+
+ let logViewHelper: LogViewHelper?
+ var logEntries = [LogViewHelper.LogEntry]()
+ var isFetchingLogEntries = false
+ var isInScrolledToEndMode = true
+
+ private var updateLogEntriesTimer: Timer?
+
+ init() {
+ logViewHelper = LogViewHelper(logFilePath: FileManager.logFileURL?.path)
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func loadView() {
+ tableView.dataSource = self
+ tableView.delegate = self
+
+ closeButton.target = self
+ closeButton.action = #selector(closeClicked)
+
+ saveButton.target = self
+ saveButton.action = #selector(saveClicked)
+ saveButton.isEnabled = false
+
+ let clipView = NSClipView()
+ clipView.documentView = tableView
+ scrollView.contentView = clipView
+
+ _ = NotificationCenter.default.addObserver(forName: NSView.boundsDidChangeNotification, object: clipView, queue: OperationQueue.main) { [weak self] _ in
+ guard let self = self else { return }
+ let lastVisibleRowIndex = self.tableView.row(at: NSPoint(x: 0, y: self.scrollView.contentView.documentVisibleRect.maxY - 1))
+ self.isInScrolledToEndMode = lastVisibleRowIndex < 0 || lastVisibleRowIndex == self.logEntries.count - 1
+ }
+
+ _ = NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification, object: tableView, queue: OperationQueue.main) { [weak self] _ in
+ guard let self = self else { return }
+ if self.isInScrolledToEndMode {
+ DispatchQueue.main.async {
+ self.tableView.scroll(NSPoint(x: 0, y: self.tableView.frame.maxY - clipView.documentVisibleRect.height))
+ }
+ }
+ }
+
+ let margin: CGFloat = 20
+ let internalSpacing: CGFloat = 10
+
+ let buttonRowStackView = NSStackView()
+ buttonRowStackView.addView(closeButton, in: .leading)
+ buttonRowStackView.addView(saveButton, in: .trailing)
+ buttonRowStackView.orientation = .horizontal
+ buttonRowStackView.spacing = internalSpacing
+
+ let containerView = NSView()
+ [scrollView, progressIndicator, buttonRowStackView].forEach { view in
+ containerView.addSubview(view)
+ view.translatesAutoresizingMaskIntoConstraints = false
+ }
+ NSLayoutConstraint.activate([
+ scrollView.topAnchor.constraint(equalTo: containerView.topAnchor, constant: margin),
+ scrollView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: margin),
+ containerView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: margin),
+ buttonRowStackView.topAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: internalSpacing),
+ buttonRowStackView.leftAnchor.constraint(equalTo: containerView.leftAnchor, constant: margin),
+ containerView.rightAnchor.constraint(equalTo: buttonRowStackView.rightAnchor, constant: margin),
+ containerView.bottomAnchor.constraint(equalTo: buttonRowStackView.bottomAnchor, constant: margin),
+ progressIndicator.centerXAnchor.constraint(equalTo: scrollView.centerXAnchor),
+ progressIndicator.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor)
+ ])
+
+ NSLayoutConstraint.activate([
+ containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 640),
+ containerView.widthAnchor.constraint(lessThanOrEqualToConstant: 1200),
+ containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 240)
+ ])
+
+ containerView.frame = NSRect(x: 0, y: 0, width: 640, height: 480)
+
+ view = containerView
+
+ progressIndicator.startAnimation(self)
+ startUpdatingLogEntries()
+ }
+
+ func updateLogEntries() {
+ guard !isFetchingLogEntries else { return }
+ isFetchingLogEntries = true
+ logViewHelper?.fetchLogEntriesSinceLastFetch { [weak self] fetchedLogEntries in
+ guard let self = self else { return }
+ defer {
+ self.isFetchingLogEntries = false
+ }
+ if !self.progressIndicator.isHidden {
+ self.progressIndicator.stopAnimation(self)
+ self.saveButton.isEnabled = true
+ }
+ guard !fetchedLogEntries.isEmpty else { return }
+ let oldCount = self.logEntries.count
+ self.logEntries.append(contentsOf: fetchedLogEntries)
+ self.tableView.insertRows(at: IndexSet(integersIn: oldCount ..< oldCount + fetchedLogEntries.count), withAnimation: .slideDown)
+ }
+ }
+
+ func startUpdatingLogEntries() {
+ updateLogEntries()
+ updateLogEntriesTimer?.invalidate()
+ let timer = Timer(timeInterval: 1 /* second */, repeats: true) { [weak self] _ in
+ self?.updateLogEntries()
+ }
+ updateLogEntriesTimer = timer
+ RunLoop.main.add(timer, forMode: .common)
+ }
+
+ func stopUpdatingLogEntries() {
+ updateLogEntriesTimer?.invalidate()
+ updateLogEntriesTimer = nil
+ }
+
+ override func viewWillAppear() {
+ view.window?.setFrameAutosaveName(NSWindow.FrameAutosaveName("LogWindow"))
+ }
+
+ override func viewWillDisappear() {
+ super.viewWillDisappear()
+ stopUpdatingLogEntries()
+ }
+
+ @objc func saveClicked() {
+ let savePanel = NSSavePanel()
+ savePanel.prompt = tr("macSheetButtonExportLog")
+ savePanel.nameFieldLabel = tr("macNameFieldExportLog")
+
+ let dateFormatter = ISO8601DateFormatter()
+ dateFormatter.formatOptions = [.withFullDate, .withTime, .withTimeZone] // Avoid ':' in the filename
+ let timeStampString = dateFormatter.string(from: Date())
+ savePanel.nameFieldStringValue = "wireguard-log-\(timeStampString).txt"
+
+ savePanel.beginSheetModal(for: self.view.window!) { [weak self] response in
+ guard response == .OK else { return }
+ guard let destinationURL = savePanel.url else { return }
+
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
+ let isWritten = Logger.global?.writeLog(to: destinationURL.path) ?? false
+ guard isWritten else {
+ DispatchQueue.main.async { [weak self] in
+ ErrorPresenter.showErrorAlert(title: tr("alertUnableToWriteLogTitle"), message: tr("alertUnableToWriteLogMessage"), from: self)
+ }
+ return
+ }
+ DispatchQueue.main.async { [weak self] in
+ guard let self = self else { return }
+ self.presentingViewController?.dismiss(self)
+ }
+ }
+
+ }
+ }
+
+ @objc func closeClicked() {
+ presentingViewController?.dismiss(self)
+ }
+
+ @objc func copy(_ sender: Any?) {
+ let text = tableView.selectedRowIndexes.sorted().reduce("") { $0 + self.logEntries[$1].text() + "\n" }
+ let pasteboard = NSPasteboard.general
+ pasteboard.clearContents()
+ pasteboard.writeObjects([text as NSString])
+ }
+}
+
+extension LogViewController: NSTableViewDataSource {
+ func numberOfRows(in tableView: NSTableView) -> Int {
+ return logEntries.count
+ }
+}
+
+extension LogViewController: NSTableViewDelegate {
+ func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
+ if LogColumn.time.isRepresenting(tableColumn: tableColumn) {
+ let cell: LogViewTimestampCell = tableView.dequeueReusableCell()
+ cell.text = logEntries[row].timestamp
+ return cell
+ } else if LogColumn.logMessage.isRepresenting(tableColumn: tableColumn) {
+ let cell: LogViewMessageCell = tableView.dequeueReusableCell()
+ cell.text = logEntries[row].message
+ return cell
+ } else {
+ fatalError()
+ }
+ }
+}
+
+extension LogViewController {
+ override func cancelOperation(_ sender: Any?) {
+ closeClicked()
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/ViewController/ManageTunnelsRootViewController.swift b/Sources/WireGuardApp/UI/macOS/ViewController/ManageTunnelsRootViewController.swift
new file mode 100644
index 0000000..0ad0805
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/ViewController/ManageTunnelsRootViewController.swift
@@ -0,0 +1,135 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class ManageTunnelsRootViewController: NSViewController {
+
+ let tunnelsManager: TunnelsManager
+ var tunnelsListVC: TunnelsListTableViewController?
+ var tunnelDetailVC: TunnelDetailTableViewController?
+ let tunnelDetailContainerView = NSView()
+ var tunnelDetailContentVC: NSViewController?
+
+ init(tunnelsManager: TunnelsManager) {
+ self.tunnelsManager = tunnelsManager
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func loadView() {
+ view = NSView()
+
+ let horizontalSpacing: CGFloat = 20
+ let verticalSpacing: CGFloat = 20
+ let centralSpacing: CGFloat = 10
+
+ let container = NSLayoutGuide()
+ view.addLayoutGuide(container)
+ NSLayoutConstraint.activate([
+ container.topAnchor.constraint(equalTo: view.topAnchor, constant: verticalSpacing),
+ view.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: verticalSpacing),
+ container.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: horizontalSpacing),
+ view.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: horizontalSpacing)
+ ])
+
+ tunnelsListVC = TunnelsListTableViewController(tunnelsManager: tunnelsManager)
+ tunnelsListVC!.delegate = self
+ let tunnelsListView = tunnelsListVC!.view
+
+ addChild(tunnelsListVC!)
+ view.addSubview(tunnelsListView)
+ view.addSubview(tunnelDetailContainerView)
+
+ tunnelsListView.translatesAutoresizingMaskIntoConstraints = false
+ tunnelDetailContainerView.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ tunnelsListView.topAnchor.constraint(equalTo: container.topAnchor),
+ tunnelsListView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
+ tunnelsListView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
+ tunnelDetailContainerView.topAnchor.constraint(equalTo: container.topAnchor),
+ tunnelDetailContainerView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
+ tunnelDetailContainerView.leadingAnchor.constraint(equalTo: tunnelsListView.trailingAnchor, constant: centralSpacing),
+ tunnelDetailContainerView.trailingAnchor.constraint(equalTo: container.trailingAnchor)
+ ])
+ }
+
+ private func setTunnelDetailContentVC(_ contentVC: NSViewController) {
+ if let currentContentVC = tunnelDetailContentVC {
+ currentContentVC.view.removeFromSuperview()
+ currentContentVC.removeFromParent()
+ }
+ addChild(contentVC)
+ tunnelDetailContainerView.addSubview(contentVC.view)
+ contentVC.view.translatesAutoresizingMaskIntoConstraints = false
+ NSLayoutConstraint.activate([
+ tunnelDetailContainerView.topAnchor.constraint(equalTo: contentVC.view.topAnchor),
+ tunnelDetailContainerView.bottomAnchor.constraint(equalTo: contentVC.view.bottomAnchor),
+ tunnelDetailContainerView.leadingAnchor.constraint(equalTo: contentVC.view.leadingAnchor),
+ tunnelDetailContainerView.trailingAnchor.constraint(equalTo: contentVC.view.trailingAnchor)
+ ])
+ tunnelDetailContentVC = contentVC
+ }
+}
+
+extension ManageTunnelsRootViewController: TunnelsListTableViewControllerDelegate {
+ func tunnelsSelected(tunnelIndices: [Int]) {
+ assert(!tunnelIndices.isEmpty)
+ if tunnelIndices.count == 1 {
+ let tunnel = tunnelsManager.tunnel(at: tunnelIndices.first!)
+ if tunnel.isTunnelAvailableToUser {
+ let tunnelDetailVC = TunnelDetailTableViewController(tunnelsManager: tunnelsManager, tunnel: tunnel)
+ setTunnelDetailContentVC(tunnelDetailVC)
+ self.tunnelDetailVC = tunnelDetailVC
+ } else {
+ let unusableTunnelDetailVC = tunnelDetailContentVC as? UnusableTunnelDetailViewController ?? UnusableTunnelDetailViewController()
+ unusableTunnelDetailVC.onButtonClicked = { [weak tunnelsListVC] in
+ tunnelsListVC?.handleRemoveTunnelAction()
+ }
+ setTunnelDetailContentVC(unusableTunnelDetailVC)
+ self.tunnelDetailVC = nil
+ }
+ } else if tunnelIndices.count > 1 {
+ let multiSelectionVC = tunnelDetailContentVC as? ButtonedDetailViewController ?? ButtonedDetailViewController()
+ multiSelectionVC.setButtonTitle(tr(format: "macButtonDeleteTunnels (%d)", tunnelIndices.count))
+ multiSelectionVC.onButtonClicked = { [weak tunnelsListVC] in
+ tunnelsListVC?.handleRemoveTunnelAction()
+ }
+ setTunnelDetailContentVC(multiSelectionVC)
+ self.tunnelDetailVC = nil
+ }
+ }
+
+ func tunnelsListEmpty() {
+ let noTunnelsVC = ButtonedDetailViewController()
+ noTunnelsVC.setButtonTitle(tr("macButtonImportTunnels"))
+ noTunnelsVC.onButtonClicked = { [weak self] in
+ guard let self = self else { return }
+ ImportPanelPresenter.presentImportPanel(tunnelsManager: self.tunnelsManager, sourceVC: self)
+ }
+ setTunnelDetailContentVC(noTunnelsVC)
+ self.tunnelDetailVC = nil
+ }
+}
+
+extension ManageTunnelsRootViewController {
+ override func supplementalTarget(forAction action: Selector, sender: Any?) -> Any? {
+ switch action {
+ case #selector(TunnelsListTableViewController.handleViewLogAction),
+ #selector(TunnelsListTableViewController.handleAddEmptyTunnelAction),
+ #selector(TunnelsListTableViewController.handleImportTunnelAction),
+ #selector(TunnelsListTableViewController.handleExportTunnelsAction),
+ #selector(TunnelsListTableViewController.handleRemoveTunnelAction):
+ return tunnelsListVC
+ case #selector(TunnelDetailTableViewController.handleToggleActiveStatusAction),
+ #selector(TunnelDetailTableViewController.handleEditTunnelAction):
+ return tunnelDetailVC
+ default:
+ return super.supplementalTarget(forAction: action, sender: sender)
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/ViewController/TunnelDetailTableViewController.swift b/Sources/WireGuardApp/UI/macOS/ViewController/TunnelDetailTableViewController.swift
new file mode 100644
index 0000000..80b759e
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/ViewController/TunnelDetailTableViewController.swift
@@ -0,0 +1,516 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+import WireGuardKit
+
+class TunnelDetailTableViewController: NSViewController {
+
+ private enum TableViewModelRow {
+ case interfaceFieldRow(TunnelViewModel.InterfaceField)
+ case peerFieldRow(peer: TunnelViewModel.PeerData, field: TunnelViewModel.PeerField)
+ case onDemandRow
+ case onDemandSSIDRow
+ case spacerRow
+
+ func localizedSectionKeyString() -> String {
+ switch self {
+ case .interfaceFieldRow: return tr("tunnelSectionTitleInterface")
+ case .peerFieldRow: return tr("tunnelSectionTitlePeer")
+ case .onDemandRow: return tr("macFieldOnDemand")
+ case .onDemandSSIDRow: return ""
+ case .spacerRow: return ""
+ }
+ }
+
+ func isTitleRow() -> Bool {
+ switch self {
+ case .interfaceFieldRow(let field): return field == .name
+ case .peerFieldRow(_, let field): return field == .publicKey
+ case .onDemandRow: return true
+ case .onDemandSSIDRow: return false
+ case .spacerRow: return false
+ }
+ }
+ }
+
+ static let interfaceFields: [TunnelViewModel.InterfaceField] = [
+ .name, .status, .publicKey, .addresses,
+ .listenPort, .mtu, .dns, .toggleStatus
+ ]
+
+ static let peerFields: [TunnelViewModel.PeerField] = [
+ .publicKey, .preSharedKey, .endpoint,
+ .allowedIPs, .persistentKeepAlive,
+ .rxBytes, .txBytes, .lastHandshakeTime
+ ]
+
+ static let onDemandFields: [ActivateOnDemandViewModel.OnDemandField] = [
+ .onDemand, .ssid
+ ]
+
+ let tableView: NSTableView = {
+ let tableView = NSTableView()
+ tableView.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("TunnelDetail")))
+ tableView.headerView = nil
+ tableView.rowSizeStyle = .medium
+ tableView.backgroundColor = .clear
+ tableView.selectionHighlightStyle = .none
+ return tableView
+ }()
+
+ let editButton: NSButton = {
+ let button = NSButton()
+ button.title = tr("macButtonEdit")
+ button.setButtonType(.momentaryPushIn)
+ button.bezelStyle = .rounded
+ button.toolTip = tr("macToolTipEditTunnel")
+ return button
+ }()
+
+ let box: NSBox = {
+ let box = NSBox()
+ box.titlePosition = .noTitle
+ box.fillColor = .unemphasizedSelectedContentBackgroundColor
+ return box
+ }()
+
+ let tunnelsManager: TunnelsManager
+ let tunnel: TunnelContainer
+
+ var tunnelViewModel: TunnelViewModel {
+ didSet {
+ updateTableViewModelRowsBySection()
+ updateTableViewModelRows()
+ }
+ }
+
+ var onDemandViewModel: ActivateOnDemandViewModel
+
+ private var tableViewModelRowsBySection = [[(isVisible: Bool, modelRow: TableViewModelRow)]]()
+ private var tableViewModelRows = [TableViewModelRow]()
+
+ private var statusObservationToken: AnyObject?
+ private var tunnelEditVC: TunnelEditViewController?
+ private var reloadRuntimeConfigurationTimer: Timer?
+
+ init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer) {
+ self.tunnelsManager = tunnelsManager
+ self.tunnel = tunnel
+ tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
+ onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
+ super.init(nibName: nil, bundle: nil)
+ updateTableViewModelRowsBySection()
+ updateTableViewModelRows()
+ statusObservationToken = tunnel.observe(\TunnelContainer.status) { [weak self] _, _ in
+ guard let self = self else { return }
+ if tunnel.status == .active {
+ self.startUpdatingRuntimeConfiguration()
+ } else if tunnel.status == .inactive {
+ self.reloadRuntimeConfiguration()
+ self.stopUpdatingRuntimeConfiguration()
+ }
+ }
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func loadView() {
+ tableView.dataSource = self
+ tableView.delegate = self
+
+ editButton.target = self
+ editButton.action = #selector(handleEditTunnelAction)
+
+ let clipView = NSClipView()
+ clipView.documentView = tableView
+
+ let scrollView = NSScrollView()
+ scrollView.contentView = clipView // Set contentView before setting drawsBackground
+ scrollView.drawsBackground = false
+ scrollView.hasVerticalScroller = true
+ scrollView.autohidesScrollers = true
+
+ let containerView = NSView()
+ let bottomControlsContainer = NSLayoutGuide()
+ containerView.addLayoutGuide(bottomControlsContainer)
+ containerView.addSubview(box)
+ containerView.addSubview(scrollView)
+ containerView.addSubview(editButton)
+ box.translatesAutoresizingMaskIntoConstraints = false
+ scrollView.translatesAutoresizingMaskIntoConstraints = false
+ editButton.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ containerView.topAnchor.constraint(equalTo: scrollView.topAnchor),
+ containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
+ containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
+ containerView.leadingAnchor.constraint(equalTo: bottomControlsContainer.leadingAnchor),
+ containerView.trailingAnchor.constraint(equalTo: bottomControlsContainer.trailingAnchor),
+ bottomControlsContainer.heightAnchor.constraint(equalToConstant: 32),
+ scrollView.bottomAnchor.constraint(equalTo: bottomControlsContainer.topAnchor),
+ bottomControlsContainer.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
+ editButton.trailingAnchor.constraint(equalTo: bottomControlsContainer.trailingAnchor),
+ bottomControlsContainer.bottomAnchor.constraint(equalTo: editButton.bottomAnchor, constant: 0)
+ ])
+
+ NSLayoutConstraint.activate([
+ scrollView.topAnchor.constraint(equalTo: box.topAnchor),
+ scrollView.bottomAnchor.constraint(equalTo: box.bottomAnchor),
+ scrollView.leadingAnchor.constraint(equalTo: box.leadingAnchor),
+ scrollView.trailingAnchor.constraint(equalTo: box.trailingAnchor)
+ ])
+
+ NSLayoutConstraint.activate([
+ containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 320),
+ containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 120)
+ ])
+
+ view = containerView
+ }
+
+ func updateTableViewModelRowsBySection() {
+ var modelRowsBySection = [[(isVisible: Bool, modelRow: TableViewModelRow)]]()
+
+ var interfaceSection = [(isVisible: Bool, modelRow: TableViewModelRow)]()
+ for field in TunnelDetailTableViewController.interfaceFields {
+ let isStatus = field == .status || field == .toggleStatus
+ let isEmpty = tunnelViewModel.interfaceData[field].isEmpty
+ interfaceSection.append((isVisible: isStatus || !isEmpty, modelRow: .interfaceFieldRow(field)))
+ }
+ interfaceSection.append((isVisible: true, modelRow: .spacerRow))
+ modelRowsBySection.append(interfaceSection)
+
+ for peerData in tunnelViewModel.peersData {
+ var peerSection = [(isVisible: Bool, modelRow: TableViewModelRow)]()
+ for field in TunnelDetailTableViewController.peerFields {
+ peerSection.append((isVisible: !peerData[field].isEmpty, modelRow: .peerFieldRow(peer: peerData, field: field)))
+ }
+ peerSection.append((isVisible: true, modelRow: .spacerRow))
+ modelRowsBySection.append(peerSection)
+ }
+
+ var onDemandSection = [(isVisible: Bool, modelRow: TableViewModelRow)]()
+ onDemandSection.append((isVisible: true, modelRow: .onDemandRow))
+ if onDemandViewModel.isWiFiInterfaceEnabled {
+ onDemandSection.append((isVisible: true, modelRow: .onDemandSSIDRow))
+ }
+ modelRowsBySection.append(onDemandSection)
+
+ tableViewModelRowsBySection = modelRowsBySection
+ }
+
+ func updateTableViewModelRows() {
+ tableViewModelRows = tableViewModelRowsBySection.flatMap { $0.filter { $0.isVisible }.map { $0.modelRow } }
+ }
+
+ @objc func handleEditTunnelAction() {
+ PrivateDataConfirmation.confirmAccess(to: tr("macViewPrivateData")) { [weak self] in
+ guard let self = self else { return }
+ let tunnelEditVC = TunnelEditViewController(tunnelsManager: self.tunnelsManager, tunnel: self.tunnel)
+ tunnelEditVC.delegate = self
+ self.presentAsSheet(tunnelEditVC)
+ self.tunnelEditVC = tunnelEditVC
+ }
+ }
+
+ @objc func handleToggleActiveStatusAction() {
+ if tunnel.status == .inactive {
+ tunnelsManager.startActivation(of: tunnel)
+ } else if tunnel.status == .active {
+ tunnelsManager.startDeactivation(of: tunnel)
+ }
+ }
+
+ override func viewWillAppear() {
+ if tunnel.status == .active {
+ startUpdatingRuntimeConfiguration()
+ }
+ }
+
+ override func viewWillDisappear() {
+ super.viewWillDisappear()
+ if let tunnelEditVC = tunnelEditVC {
+ dismiss(tunnelEditVC)
+ }
+ stopUpdatingRuntimeConfiguration()
+ }
+
+ func applyTunnelConfiguration(tunnelConfiguration: TunnelConfiguration) {
+ // Incorporates changes from tunnelConfiguation. Ignores any changes in peer ordering.
+
+ let tableView = self.tableView
+
+ func handleSectionFieldsModified<T>(fields: [T], modelRowsInSection: [(isVisible: Bool, modelRow: TableViewModelRow)], rowOffset: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
+ var modifiedRowIndices = IndexSet()
+ for (index, field) in fields.enumerated() {
+ guard let change = changes[field] else { continue }
+ if case .modified(_) = change {
+ let row = modelRowsInSection[0 ..< index].filter { $0.isVisible }.count
+ modifiedRowIndices.insert(rowOffset + row)
+ }
+ }
+ if !modifiedRowIndices.isEmpty {
+ tableView.reloadData(forRowIndexes: modifiedRowIndices, columnIndexes: IndexSet(integer: 0))
+ }
+ }
+
+ func handleSectionFieldsAddedOrRemoved<T>(fields: [T], modelRowsInSection: inout [(isVisible: Bool, modelRow: TableViewModelRow)], rowOffset: Int, changes: [T: TunnelViewModel.Changes.FieldChange]) {
+ for (index, field) in fields.enumerated() {
+ guard let change = changes[field] else { continue }
+ let row = modelRowsInSection[0 ..< index].filter { $0.isVisible }.count
+ switch change {
+ case .added:
+ tableView.insertRows(at: IndexSet(integer: rowOffset + row), withAnimation: .effectFade)
+ modelRowsInSection[index].isVisible = true
+ case .removed:
+ tableView.removeRows(at: IndexSet(integer: rowOffset + row), withAnimation: .effectFade)
+ modelRowsInSection[index].isVisible = false
+ case .modified:
+ break
+ }
+ }
+ }
+
+ let changes = self.tunnelViewModel.applyConfiguration(other: tunnelConfiguration)
+
+ if !changes.interfaceChanges.isEmpty {
+ handleSectionFieldsModified(fields: TunnelDetailTableViewController.interfaceFields,
+ modelRowsInSection: self.tableViewModelRowsBySection[0],
+ rowOffset: 0, changes: changes.interfaceChanges)
+ }
+ for (peerIndex, peerChanges) in changes.peerChanges {
+ let sectionIndex = 1 + peerIndex
+ let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
+ handleSectionFieldsModified(fields: TunnelDetailTableViewController.peerFields,
+ modelRowsInSection: self.tableViewModelRowsBySection[sectionIndex],
+ rowOffset: rowOffset, changes: peerChanges)
+ }
+
+ let isAnyInterfaceFieldAddedOrRemoved = changes.interfaceChanges.contains { $0.value == .added || $0.value == .removed }
+ let isAnyPeerFieldAddedOrRemoved = changes.peerChanges.contains { $0.changes.contains { $0.value == .added || $0.value == .removed } }
+
+ if isAnyInterfaceFieldAddedOrRemoved || isAnyPeerFieldAddedOrRemoved || !changes.peersRemovedIndices.isEmpty || !changes.peersInsertedIndices.isEmpty {
+ tableView.beginUpdates()
+ if isAnyInterfaceFieldAddedOrRemoved {
+ handleSectionFieldsAddedOrRemoved(fields: TunnelDetailTableViewController.interfaceFields,
+ modelRowsInSection: &self.tableViewModelRowsBySection[0],
+ rowOffset: 0, changes: changes.interfaceChanges)
+ }
+ if isAnyPeerFieldAddedOrRemoved {
+ for (peerIndex, peerChanges) in changes.peerChanges {
+ let sectionIndex = 1 + peerIndex
+ let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
+ handleSectionFieldsAddedOrRemoved(fields: TunnelDetailTableViewController.peerFields, modelRowsInSection: &self.tableViewModelRowsBySection[sectionIndex], rowOffset: rowOffset, changes: peerChanges)
+ }
+ }
+ if !changes.peersRemovedIndices.isEmpty {
+ for peerIndex in changes.peersRemovedIndices {
+ let sectionIndex = 1 + peerIndex
+ let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
+ let count = self.tableViewModelRowsBySection[sectionIndex].filter { $0.isVisible }.count
+ self.tableView.removeRows(at: IndexSet(integersIn: rowOffset ..< rowOffset + count), withAnimation: .effectFade)
+ self.tableViewModelRowsBySection.remove(at: sectionIndex)
+ }
+ }
+ if !changes.peersInsertedIndices.isEmpty {
+ for peerIndex in changes.peersInsertedIndices {
+ let peerData = self.tunnelViewModel.peersData[peerIndex]
+ let sectionIndex = 1 + peerIndex
+ let rowOffset = self.tableViewModelRowsBySection[0 ..< sectionIndex].flatMap { $0.filter { $0.isVisible } }.count
+ var modelRowsInSection: [(isVisible: Bool, modelRow: TableViewModelRow)] = TunnelDetailTableViewController.peerFields.map {
+ (isVisible: !peerData[$0].isEmpty, modelRow: .peerFieldRow(peer: peerData, field: $0))
+ }
+ modelRowsInSection.append((isVisible: true, modelRow: .spacerRow))
+ let count = modelRowsInSection.filter { $0.isVisible }.count
+ self.tableView.insertRows(at: IndexSet(integersIn: rowOffset ..< rowOffset + count), withAnimation: .effectFade)
+ self.tableViewModelRowsBySection.insert(modelRowsInSection, at: sectionIndex)
+ }
+ }
+ updateTableViewModelRows()
+ tableView.endUpdates()
+ }
+ }
+
+ private func reloadRuntimeConfiguration() {
+ tunnel.getRuntimeTunnelConfiguration { [weak self] tunnelConfiguration in
+ guard let tunnelConfiguration = tunnelConfiguration else { return }
+ self?.applyTunnelConfiguration(tunnelConfiguration: tunnelConfiguration)
+ }
+ }
+
+ func startUpdatingRuntimeConfiguration() {
+ reloadRuntimeConfiguration()
+ reloadRuntimeConfigurationTimer?.invalidate()
+ let reloadTimer = Timer(timeInterval: 1 /* second */, repeats: true) { [weak self] _ in
+ self?.reloadRuntimeConfiguration()
+ }
+ reloadRuntimeConfigurationTimer = reloadTimer
+ RunLoop.main.add(reloadTimer, forMode: .common)
+ }
+
+ func stopUpdatingRuntimeConfiguration() {
+ reloadRuntimeConfiguration()
+ reloadRuntimeConfigurationTimer?.invalidate()
+ reloadRuntimeConfigurationTimer = nil
+ }
+
+}
+
+extension TunnelDetailTableViewController: NSTableViewDataSource {
+ func numberOfRows(in tableView: NSTableView) -> Int {
+ return tableViewModelRows.count
+ }
+}
+
+extension TunnelDetailTableViewController: NSTableViewDelegate {
+ func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
+ let modelRow = tableViewModelRows[row]
+ switch modelRow {
+ case .interfaceFieldRow(let field):
+ if field == .status {
+ return statusCell()
+ } else if field == .toggleStatus {
+ return toggleStatusCell()
+ } else {
+ let cell: KeyValueRow = tableView.dequeueReusableCell()
+ let localizedKeyString = modelRow.isTitleRow() ? modelRow.localizedSectionKeyString() : field.localizedUIString
+ cell.key = tr(format: "macFieldKey (%@)", localizedKeyString)
+ cell.value = tunnelViewModel.interfaceData[field]
+ cell.isKeyInBold = modelRow.isTitleRow()
+ return cell
+ }
+ case .peerFieldRow(let peerData, let field):
+ let cell: KeyValueRow = tableView.dequeueReusableCell()
+ let localizedKeyString = modelRow.isTitleRow() ? modelRow.localizedSectionKeyString() : field.localizedUIString
+ cell.key = tr(format: "macFieldKey (%@)", localizedKeyString)
+ if field == .persistentKeepAlive {
+ cell.value = tr(format: "tunnelPeerPersistentKeepaliveValue (%@)", peerData[field])
+ } else if field == .preSharedKey {
+ cell.value = tr("tunnelPeerPresharedKeyEnabled")
+ } else {
+ cell.value = peerData[field]
+ }
+ cell.isKeyInBold = modelRow.isTitleRow()
+ return cell
+ case .spacerRow:
+ return NSView()
+ case .onDemandRow:
+ let cell: KeyValueRow = tableView.dequeueReusableCell()
+ cell.key = modelRow.localizedSectionKeyString()
+ cell.value = onDemandViewModel.localizedInterfaceDescription
+ cell.isKeyInBold = true
+ return cell
+ case .onDemandSSIDRow:
+ let cell: KeyValueRow = tableView.dequeueReusableCell()
+ cell.key = tr("macFieldOnDemandSSIDs")
+ let value: String
+ if onDemandViewModel.ssidOption == .anySSID {
+ value = onDemandViewModel.ssidOption.localizedUIString
+ } else {
+ value = tr(format: "tunnelOnDemandSSIDOptionDescriptionMac (%1$@: %2$@)",
+ onDemandViewModel.ssidOption.localizedUIString,
+ onDemandViewModel.selectedSSIDs.joined(separator: ", "))
+ }
+ cell.value = value
+ cell.isKeyInBold = false
+ return cell
+ }
+ }
+
+ func statusCell() -> NSView {
+ let cell: KeyValueImageRow = tableView.dequeueReusableCell()
+ cell.key = tr(format: "macFieldKey (%@)", tr("tunnelInterfaceStatus"))
+ cell.value = TunnelDetailTableViewController.localizedStatusDescription(forStatus: tunnel.status)
+ cell.valueImage = TunnelDetailTableViewController.image(forStatus: tunnel.status)
+ cell.observationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
+ guard let cell = cell else { return }
+ cell.value = TunnelDetailTableViewController.localizedStatusDescription(forStatus: tunnel.status)
+ cell.valueImage = TunnelDetailTableViewController.image(forStatus: tunnel.status)
+ }
+ return cell
+ }
+
+ func toggleStatusCell() -> NSView {
+ let cell: ButtonRow = tableView.dequeueReusableCell()
+ cell.buttonTitle = TunnelDetailTableViewController.localizedToggleStatusActionText(forStatus: tunnel.status)
+ cell.isButtonEnabled = (tunnel.status == .active || tunnel.status == .inactive)
+ cell.buttonToolTip = tr("macToolTipToggleStatus")
+ cell.onButtonClicked = { [weak self] in
+ self?.handleToggleActiveStatusAction()
+ }
+ cell.observationToken = tunnel.observe(\.status) { [weak cell] tunnel, _ in
+ guard let cell = cell else { return }
+ cell.buttonTitle = TunnelDetailTableViewController.localizedToggleStatusActionText(forStatus: tunnel.status)
+ cell.isButtonEnabled = (tunnel.status == .active || tunnel.status == .inactive)
+ }
+ return cell
+ }
+
+ private static func localizedStatusDescription(forStatus status: TunnelStatus) -> String {
+ switch status {
+ case .inactive:
+ return tr("tunnelStatusInactive")
+ case .activating:
+ return tr("tunnelStatusActivating")
+ case .active:
+ return tr("tunnelStatusActive")
+ case .deactivating:
+ return tr("tunnelStatusDeactivating")
+ case .reasserting:
+ return tr("tunnelStatusReasserting")
+ case .restarting:
+ return tr("tunnelStatusRestarting")
+ case .waiting:
+ return tr("tunnelStatusWaiting")
+ }
+ }
+
+ private static func image(forStatus status: TunnelStatus?) -> NSImage? {
+ guard let status = status else { return nil }
+ switch status {
+ case .active, .restarting, .reasserting:
+ return NSImage(named: NSImage.statusAvailableName)
+ case .activating, .waiting, .deactivating:
+ return NSImage(named: NSImage.statusPartiallyAvailableName)
+ case .inactive:
+ return NSImage(named: NSImage.statusNoneName)
+ }
+ }
+
+ private static func localizedToggleStatusActionText(forStatus status: TunnelStatus) -> String {
+ switch status {
+ case .waiting:
+ return tr("macToggleStatusButtonWaiting")
+ case .inactive:
+ return tr("macToggleStatusButtonActivate")
+ case .activating:
+ return tr("macToggleStatusButtonActivating")
+ case .active:
+ return tr("macToggleStatusButtonDeactivate")
+ case .deactivating:
+ return tr("macToggleStatusButtonDeactivating")
+ case .reasserting:
+ return tr("macToggleStatusButtonReasserting")
+ case .restarting:
+ return tr("macToggleStatusButtonRestarting")
+ }
+ }
+}
+
+extension TunnelDetailTableViewController: TunnelEditViewControllerDelegate {
+ func tunnelSaved(tunnel: TunnelContainer) {
+ tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnel.tunnelConfiguration)
+ onDemandViewModel = ActivateOnDemandViewModel(tunnel: tunnel)
+ updateTableViewModelRowsBySection()
+ updateTableViewModelRows()
+ tableView.reloadData()
+ self.tunnelEditVC = nil
+ }
+
+ func tunnelEditingCancelled() {
+ self.tunnelEditVC = nil
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/ViewController/TunnelEditViewController.swift b/Sources/WireGuardApp/UI/macOS/ViewController/TunnelEditViewController.swift
new file mode 100644
index 0000000..97eaf8f
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/ViewController/TunnelEditViewController.swift
@@ -0,0 +1,297 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+import WireGuardKit
+
+protocol TunnelEditViewControllerDelegate: class {
+ func tunnelSaved(tunnel: TunnelContainer)
+ func tunnelEditingCancelled()
+}
+
+class TunnelEditViewController: NSViewController {
+
+ let nameRow: EditableKeyValueRow = {
+ let nameRow = EditableKeyValueRow()
+ nameRow.key = tr(format: "macFieldKey (%@)", TunnelViewModel.InterfaceField.name.localizedUIString)
+ return nameRow
+ }()
+
+ let publicKeyRow: KeyValueRow = {
+ let publicKeyRow = KeyValueRow()
+ publicKeyRow.key = tr(format: "macFieldKey (%@)", TunnelViewModel.InterfaceField.publicKey.localizedUIString)
+ return publicKeyRow
+ }()
+
+ let textView: ConfTextView = {
+ let textView = ConfTextView()
+ let minWidth: CGFloat = 120
+ let minHeight: CGFloat = 0
+ textView.minSize = NSSize(width: 0, height: minHeight)
+ textView.maxSize = NSSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
+ textView.autoresizingMask = [.width] // Width should be based on superview width
+ textView.isHorizontallyResizable = false // Width shouldn't be based on content
+ textView.isVerticallyResizable = true // Height should be based on content
+ if let textContainer = textView.textContainer {
+ textContainer.size = NSSize(width: minWidth, height: CGFloat.greatestFiniteMagnitude)
+ textContainer.widthTracksTextView = true
+ }
+ NSLayoutConstraint.activate([
+ textView.widthAnchor.constraint(greaterThanOrEqualToConstant: minWidth),
+ textView.heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight)
+ ])
+ return textView
+ }()
+
+ let onDemandControlsRow = OnDemandControlsRow()
+
+ let scrollView: NSScrollView = {
+ let scrollView = NSScrollView()
+ scrollView.hasVerticalScroller = true
+ scrollView.autohidesScrollers = true
+ scrollView.borderType = .bezelBorder
+ return scrollView
+ }()
+
+ let excludePrivateIPsCheckbox: NSButton = {
+ let checkbox = NSButton()
+ checkbox.title = tr("tunnelPeerExcludePrivateIPs")
+ checkbox.setButtonType(.switch)
+ checkbox.state = .off
+ return checkbox
+ }()
+
+ let discardButton: NSButton = {
+ let button = NSButton()
+ button.title = tr("macEditDiscard")
+ button.setButtonType(.momentaryPushIn)
+ button.bezelStyle = .rounded
+ return button
+ }()
+
+ let saveButton: NSButton = {
+ let button = NSButton()
+ button.title = tr("macEditSave")
+ button.setButtonType(.momentaryPushIn)
+ button.bezelStyle = .rounded
+ button.keyEquivalent = "s"
+ button.keyEquivalentModifierMask = [.command]
+ return button
+ }()
+
+ let tunnelsManager: TunnelsManager
+ let tunnel: TunnelContainer?
+ var onDemandViewModel: ActivateOnDemandViewModel
+
+ weak var delegate: TunnelEditViewControllerDelegate?
+
+ var privateKeyObservationToken: AnyObject?
+ var hasErrorObservationToken: AnyObject?
+ var singlePeerAllowedIPsObservationToken: AnyObject?
+
+ var dnsServersAddedToAllowedIPs: String?
+
+ init(tunnelsManager: TunnelsManager, tunnel: TunnelContainer?) {
+ self.tunnelsManager = tunnelsManager
+ self.tunnel = tunnel
+ self.onDemandViewModel = tunnel != nil ? ActivateOnDemandViewModel(tunnel: tunnel!) : ActivateOnDemandViewModel()
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ func populateFields() {
+ if let tunnel = tunnel {
+ // Editing an existing tunnel
+ let tunnelConfiguration = tunnel.tunnelConfiguration!
+ nameRow.value = tunnel.name
+ textView.string = tunnelConfiguration.asWgQuickConfig()
+ publicKeyRow.value = tunnelConfiguration.interface.privateKey.publicKey.base64Key
+ textView.privateKeyString = tunnelConfiguration.interface.privateKey.base64Key
+ let singlePeer = tunnelConfiguration.peers.count == 1 ? tunnelConfiguration.peers.first : nil
+ updateExcludePrivateIPsVisibility(singlePeerAllowedIPs: singlePeer?.allowedIPs.map { $0.stringRepresentation })
+ dnsServersAddedToAllowedIPs = excludePrivateIPsCheckbox.state == .on ? tunnelConfiguration.interface.dns.map { $0.stringRepresentation }.joined(separator: ", ") : nil
+ } else {
+ // Creating a new tunnel
+ let privateKey = PrivateKey()
+ let bootstrappingText = "[Interface]\nPrivateKey = \(privateKey.base64Key)\n"
+ publicKeyRow.value = privateKey.publicKey.base64Key
+ textView.string = bootstrappingText
+ updateExcludePrivateIPsVisibility(singlePeerAllowedIPs: nil)
+ dnsServersAddedToAllowedIPs = nil
+ }
+ privateKeyObservationToken = textView.observe(\.privateKeyString) { [weak publicKeyRow] textView, _ in
+ if let privateKeyString = textView.privateKeyString,
+ let privateKey = PrivateKey(base64Key: privateKeyString) {
+ publicKeyRow?.value = privateKey.publicKey.base64Key
+ } else {
+ publicKeyRow?.value = ""
+ }
+ }
+ hasErrorObservationToken = textView.observe(\.hasError) { [weak saveButton] textView, _ in
+ saveButton?.isEnabled = !textView.hasError
+ }
+ singlePeerAllowedIPsObservationToken = textView.observe(\.singlePeerAllowedIPs) { [weak self] textView, _ in
+ self?.updateExcludePrivateIPsVisibility(singlePeerAllowedIPs: textView.singlePeerAllowedIPs)
+ }
+ }
+
+ override func loadView() {
+ populateFields()
+
+ scrollView.documentView = textView
+
+ saveButton.target = self
+ saveButton.action = #selector(handleSaveAction)
+
+ discardButton.target = self
+ discardButton.action = #selector(handleDiscardAction)
+
+ excludePrivateIPsCheckbox.target = self
+ excludePrivateIPsCheckbox.action = #selector(excludePrivateIPsCheckboxToggled(sender:))
+
+ onDemandControlsRow.onDemandViewModel = onDemandViewModel
+
+ let margin: CGFloat = 20
+ let internalSpacing: CGFloat = 10
+
+ let editorStackView = NSStackView(views: [nameRow, publicKeyRow, onDemandControlsRow, scrollView])
+ editorStackView.orientation = .vertical
+ editorStackView.setHuggingPriority(.defaultHigh, for: .horizontal)
+ editorStackView.spacing = internalSpacing
+
+ let buttonRowStackView = NSStackView()
+ buttonRowStackView.setViews([discardButton, saveButton], in: .trailing)
+ buttonRowStackView.addView(excludePrivateIPsCheckbox, in: .leading)
+ buttonRowStackView.orientation = .horizontal
+ buttonRowStackView.spacing = internalSpacing
+
+ let containerView = NSStackView(views: [editorStackView, buttonRowStackView])
+ containerView.orientation = .vertical
+ containerView.edgeInsets = NSEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)
+ containerView.setHuggingPriority(.defaultHigh, for: .horizontal)
+ containerView.spacing = internalSpacing
+
+ NSLayoutConstraint.activate([
+ containerView.widthAnchor.constraint(greaterThanOrEqualToConstant: 180),
+ containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 240)
+ ])
+ containerView.frame = NSRect(x: 0, y: 0, width: 600, height: 480)
+
+ self.view = containerView
+ }
+
+ func setUserInteractionEnabled(_ enabled: Bool) {
+ view.window?.ignoresMouseEvents = !enabled
+ nameRow.valueLabel.isEditable = enabled
+ textView.isEditable = enabled
+ onDemandControlsRow.onDemandSSIDsField.isEnabled = enabled
+ }
+
+ @objc func handleSaveAction() {
+ let name = nameRow.value
+ guard !name.isEmpty else {
+ ErrorPresenter.showErrorAlert(title: tr("macAlertNameIsEmpty"), message: "", from: self)
+ return
+ }
+
+ onDemandControlsRow.saveToViewModel()
+ let onDemandOption = onDemandViewModel.toOnDemandOption()
+
+ let isTunnelModifiedWithoutChangingName = (tunnel != nil && tunnel!.name == name)
+ guard isTunnelModifiedWithoutChangingName || tunnelsManager.tunnel(named: name) == nil else {
+ ErrorPresenter.showErrorAlert(title: tr(format: "macAlertDuplicateName (%@)", name), message: "", from: self)
+ return
+ }
+
+ var tunnelConfiguration: TunnelConfiguration
+ do {
+ tunnelConfiguration = try TunnelConfiguration(fromWgQuickConfig: textView.string, called: nameRow.value)
+ } catch let error as WireGuardAppError {
+ ErrorPresenter.showErrorAlert(error: error, from: self)
+ return
+ } catch {
+ fatalError()
+ }
+
+ if excludePrivateIPsCheckbox.state == .on, tunnelConfiguration.peers.count == 1, let dnsServersAddedToAllowedIPs = dnsServersAddedToAllowedIPs {
+ // Update the DNS servers in the AllowedIPs
+ let tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
+ let originalAllowedIPs = tunnelViewModel.peersData[0][.allowedIPs].splitToArray(trimmingCharacters: .whitespacesAndNewlines)
+ let dnsServersInAllowedIPs = TunnelViewModel.PeerData.normalizedIPAddressRangeStrings(dnsServersAddedToAllowedIPs.splitToArray(trimmingCharacters: .whitespacesAndNewlines))
+ let dnsServersCurrent = TunnelViewModel.PeerData.normalizedIPAddressRangeStrings(tunnelViewModel.interfaceData[.dns].splitToArray(trimmingCharacters: .whitespacesAndNewlines))
+ let modifiedAllowedIPs = originalAllowedIPs.filter { !dnsServersInAllowedIPs.contains($0) } + dnsServersCurrent
+ tunnelViewModel.peersData[0][.allowedIPs] = modifiedAllowedIPs.joined(separator: ", ")
+ let saveResult = tunnelViewModel.save()
+ if case .saved(let modifiedTunnelConfiguration) = saveResult {
+ tunnelConfiguration = modifiedTunnelConfiguration
+ }
+ }
+
+ setUserInteractionEnabled(false)
+
+ if let tunnel = tunnel {
+ // We're modifying an existing tunnel
+ tunnelsManager.modify(tunnel: tunnel, tunnelConfiguration: tunnelConfiguration, onDemandOption: onDemandOption) { [weak self] error in
+ guard let self = self else { return }
+ self.setUserInteractionEnabled(true)
+ if let error = error {
+ ErrorPresenter.showErrorAlert(error: error, from: self)
+ return
+ }
+ self.delegate?.tunnelSaved(tunnel: tunnel)
+ self.presentingViewController?.dismiss(self)
+ }
+ } else {
+ // We're creating a new tunnel
+ self.tunnelsManager.add(tunnelConfiguration: tunnelConfiguration, onDemandOption: onDemandOption) { [weak self] result in
+ guard let self = self else { return }
+ self.setUserInteractionEnabled(true)
+ switch result {
+ case .failure(let error):
+ ErrorPresenter.showErrorAlert(error: error, from: self)
+ case .success(let tunnel):
+ self.delegate?.tunnelSaved(tunnel: tunnel)
+ self.presentingViewController?.dismiss(self)
+ }
+ }
+ }
+ }
+
+ @objc func handleDiscardAction() {
+ delegate?.tunnelEditingCancelled()
+ presentingViewController?.dismiss(self)
+ }
+
+ func updateExcludePrivateIPsVisibility(singlePeerAllowedIPs: [String]?) {
+ let shouldAllowExcludePrivateIPsControl: Bool
+ let excludePrivateIPsValue: Bool
+ if let singlePeerAllowedIPs = singlePeerAllowedIPs {
+ (shouldAllowExcludePrivateIPsControl, excludePrivateIPsValue) = TunnelViewModel.PeerData.excludePrivateIPsFieldStates(isSinglePeer: true, allowedIPs: Set<String>(singlePeerAllowedIPs))
+ } else {
+ (shouldAllowExcludePrivateIPsControl, excludePrivateIPsValue) = TunnelViewModel.PeerData.excludePrivateIPsFieldStates(isSinglePeer: false, allowedIPs: Set<String>())
+ }
+ excludePrivateIPsCheckbox.isHidden = !shouldAllowExcludePrivateIPsControl
+ excludePrivateIPsCheckbox.state = excludePrivateIPsValue ? .on : .off
+ }
+
+ @objc func excludePrivateIPsCheckboxToggled(sender: AnyObject?) {
+ guard let excludePrivateIPsCheckbox = sender as? NSButton else { return }
+ guard let tunnelConfiguration = try? TunnelConfiguration(fromWgQuickConfig: textView.string, called: nameRow.value) else { return }
+ let isOn = excludePrivateIPsCheckbox.state == .on
+ let tunnelViewModel = TunnelViewModel(tunnelConfiguration: tunnelConfiguration)
+ tunnelViewModel.peersData.first?.excludePrivateIPsValueChanged(isOn: isOn, dnsServers: tunnelViewModel.interfaceData[.dns], oldDNSServers: dnsServersAddedToAllowedIPs)
+ if let modifiedConfig = tunnelViewModel.asWgQuickConfig() {
+ textView.setConfText(modifiedConfig)
+ dnsServersAddedToAllowedIPs = isOn ? tunnelViewModel.interfaceData[.dns] : nil
+ }
+ }
+}
+
+extension TunnelEditViewController {
+ override func cancelOperation(_ sender: Any?) {
+ handleDiscardAction()
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/ViewController/TunnelsListTableViewController.swift b/Sources/WireGuardApp/UI/macOS/ViewController/TunnelsListTableViewController.swift
new file mode 100644
index 0000000..0771582
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/ViewController/TunnelsListTableViewController.swift
@@ -0,0 +1,354 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+protocol TunnelsListTableViewControllerDelegate: class {
+ func tunnelsSelected(tunnelIndices: [Int])
+ func tunnelsListEmpty()
+}
+
+class TunnelsListTableViewController: NSViewController {
+
+ let tunnelsManager: TunnelsManager
+ weak var delegate: TunnelsListTableViewControllerDelegate?
+ var isRemovingTunnelsFromWithinTheApp = false
+
+ let tableView: NSTableView = {
+ let tableView = NSTableView()
+ tableView.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("TunnelsList")))
+ tableView.headerView = nil
+ tableView.rowSizeStyle = .medium
+ tableView.allowsMultipleSelection = true
+ return tableView
+ }()
+
+ let addButton: NSPopUpButton = {
+ let imageItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
+ imageItem.image = NSImage(named: NSImage.addTemplateName)!
+
+ let menu = NSMenu()
+ menu.addItem(imageItem)
+ menu.addItem(withTitle: tr("macMenuAddEmptyTunnel"), action: #selector(handleAddEmptyTunnelAction), keyEquivalent: "n")
+ menu.addItem(withTitle: tr("macMenuImportTunnels"), action: #selector(handleImportTunnelAction), keyEquivalent: "o")
+ menu.autoenablesItems = false
+
+ let button = NSPopUpButton(frame: NSRect.zero, pullsDown: true)
+ button.menu = menu
+ button.bezelStyle = .smallSquare
+ (button.cell as? NSPopUpButtonCell)?.arrowPosition = .arrowAtBottom
+ return button
+ }()
+
+ let removeButton: NSButton = {
+ let image = NSImage(named: NSImage.removeTemplateName)!
+ let button = NSButton(image: image, target: self, action: #selector(handleRemoveTunnelAction))
+ button.bezelStyle = .smallSquare
+ button.imagePosition = .imageOnly
+ return button
+ }()
+
+ let actionButton: NSPopUpButton = {
+ let imageItem = NSMenuItem(title: "", action: nil, keyEquivalent: "")
+ imageItem.image = NSImage(named: NSImage.actionTemplateName)!
+
+ let menu = NSMenu()
+ menu.addItem(imageItem)
+ menu.addItem(withTitle: tr("macMenuViewLog"), action: #selector(handleViewLogAction), keyEquivalent: "")
+ menu.addItem(withTitle: tr("macMenuExportTunnels"), action: #selector(handleExportTunnelsAction), keyEquivalent: "")
+ menu.autoenablesItems = false
+
+ let button = NSPopUpButton(frame: NSRect.zero, pullsDown: true)
+ button.menu = menu
+ button.bezelStyle = .smallSquare
+ (button.cell as? NSPopUpButtonCell)?.arrowPosition = .arrowAtBottom
+ return button
+ }()
+
+ init(tunnelsManager: TunnelsManager) {
+ self.tunnelsManager = tunnelsManager
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func loadView() {
+ tableView.dataSource = self
+ tableView.delegate = self
+
+ tableView.doubleAction = #selector(listDoubleClicked(sender:))
+
+ let isSelected = selectTunnelInOperation() || selectTunnel(at: 0)
+ if !isSelected {
+ delegate?.tunnelsListEmpty()
+ }
+ tableView.allowsEmptySelection = false
+
+ let scrollView = NSScrollView()
+ scrollView.hasVerticalScroller = true
+ scrollView.autohidesScrollers = true
+ scrollView.borderType = .bezelBorder
+
+ let clipView = NSClipView()
+ clipView.documentView = tableView
+ scrollView.contentView = clipView
+
+ let buttonBar = NSStackView(views: [addButton, removeButton, actionButton])
+ buttonBar.orientation = .horizontal
+ buttonBar.spacing = -1
+
+ NSLayoutConstraint.activate([
+ removeButton.widthAnchor.constraint(greaterThanOrEqualToConstant: 26),
+ removeButton.topAnchor.constraint(equalTo: buttonBar.topAnchor),
+ removeButton.bottomAnchor.constraint(equalTo: buttonBar.bottomAnchor)
+ ])
+
+ let fillerButton = FillerButton()
+
+ let containerView = NSView()
+ containerView.addSubview(scrollView)
+ containerView.addSubview(buttonBar)
+ containerView.addSubview(fillerButton)
+ scrollView.translatesAutoresizingMaskIntoConstraints = false
+ buttonBar.translatesAutoresizingMaskIntoConstraints = false
+ fillerButton.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ containerView.topAnchor.constraint(equalTo: scrollView.topAnchor),
+ containerView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
+ containerView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
+ scrollView.bottomAnchor.constraint(equalTo: buttonBar.topAnchor, constant: 1),
+ containerView.leadingAnchor.constraint(equalTo: buttonBar.leadingAnchor),
+ containerView.bottomAnchor.constraint(equalTo: buttonBar.bottomAnchor),
+ scrollView.bottomAnchor.constraint(equalTo: fillerButton.topAnchor, constant: 1),
+ containerView.bottomAnchor.constraint(equalTo: fillerButton.bottomAnchor),
+ buttonBar.trailingAnchor.constraint(equalTo: fillerButton.leadingAnchor, constant: 1),
+ fillerButton.trailingAnchor.constraint(equalTo: containerView.trailingAnchor)
+ ])
+
+ NSLayoutConstraint.activate([
+ containerView.widthAnchor.constraint(equalToConstant: 180),
+ containerView.heightAnchor.constraint(greaterThanOrEqualToConstant: 120)
+ ])
+
+ addButton.menu?.items.forEach { $0.target = self }
+ actionButton.menu?.items.forEach { $0.target = self }
+
+ view = containerView
+ }
+
+ override func viewWillAppear() {
+ selectTunnelInOperation()
+ }
+
+ @discardableResult
+ func selectTunnelInOperation() -> Bool {
+ if let currentTunnel = tunnelsManager.tunnelInOperation(), let indexToSelect = tunnelsManager.index(of: currentTunnel) {
+ return selectTunnel(at: indexToSelect)
+ }
+ return false
+ }
+
+ @objc func handleAddEmptyTunnelAction() {
+ let tunnelEditVC = TunnelEditViewController(tunnelsManager: tunnelsManager, tunnel: nil)
+ tunnelEditVC.delegate = self
+ presentAsSheet(tunnelEditVC)
+ }
+
+ @objc func handleImportTunnelAction() {
+ ImportPanelPresenter.presentImportPanel(tunnelsManager: tunnelsManager, sourceVC: self)
+ }
+
+ @objc func handleRemoveTunnelAction() {
+ guard let window = view.window else { return }
+
+ let selectedTunnelIndices = tableView.selectedRowIndexes.sorted().filter { $0 >= 0 && $0 < tunnelsManager.numberOfTunnels() }
+ guard !selectedTunnelIndices.isEmpty else { return }
+ var nextSelection = selectedTunnelIndices.last! + 1
+ if nextSelection >= tunnelsManager.numberOfTunnels() {
+ nextSelection = max(selectedTunnelIndices.first! - 1, 0)
+ }
+
+ let alert = DeleteTunnelsConfirmationAlert()
+ if selectedTunnelIndices.count == 1 {
+ let firstSelectedTunnel = tunnelsManager.tunnel(at: selectedTunnelIndices.first!)
+ alert.messageText = tr(format: "macDeleteTunnelConfirmationAlertMessage (%@)", firstSelectedTunnel.name)
+ } else {
+ alert.messageText = tr(format: "macDeleteMultipleTunnelsConfirmationAlertMessage (%d)", selectedTunnelIndices.count)
+ }
+ alert.informativeText = tr("macDeleteTunnelConfirmationAlertInfo")
+ alert.onDeleteClicked = { [weak self] completion in
+ guard let self = self else { return }
+ self.selectTunnel(at: nextSelection)
+ let selectedTunnels = selectedTunnelIndices.map { self.tunnelsManager.tunnel(at: $0) }
+ self.isRemovingTunnelsFromWithinTheApp = true
+ self.tunnelsManager.removeMultiple(tunnels: selectedTunnels) { [weak self] error in
+ guard let self = self else { return }
+ self.isRemovingTunnelsFromWithinTheApp = false
+ defer { completion() }
+ if let error = error {
+ ErrorPresenter.showErrorAlert(error: error, from: self)
+ return
+ }
+ }
+ }
+ alert.beginSheetModal(for: window)
+ }
+
+ @objc func handleViewLogAction() {
+ let logVC = LogViewController()
+ self.presentAsSheet(logVC)
+ }
+
+ @objc func handleExportTunnelsAction() {
+ PrivateDataConfirmation.confirmAccess(to: tr("macExportPrivateData")) { [weak self] in
+ guard let self = self else { return }
+ guard let window = self.view.window else { return }
+ let savePanel = NSSavePanel()
+ savePanel.allowedFileTypes = ["zip"]
+ savePanel.prompt = tr("macSheetButtonExportZip")
+ savePanel.nameFieldLabel = tr("macNameFieldExportZip")
+ savePanel.nameFieldStringValue = "wireguard-export.zip"
+ let tunnelsManager = self.tunnelsManager
+ savePanel.beginSheetModal(for: window) { [weak tunnelsManager] response in
+ guard let tunnelsManager = tunnelsManager else { return }
+ guard response == .OK else { return }
+ guard let destinationURL = savePanel.url else { return }
+ let count = tunnelsManager.numberOfTunnels()
+ let tunnelConfigurations = (0 ..< count).compactMap { tunnelsManager.tunnel(at: $0).tunnelConfiguration }
+ ZipExporter.exportConfigFiles(tunnelConfigurations: tunnelConfigurations, to: destinationURL) { [weak self] error in
+ if let error = error {
+ ErrorPresenter.showErrorAlert(error: error, from: self)
+ return
+ }
+ }
+ }
+ }
+ }
+
+ @objc func listDoubleClicked(sender: AnyObject) {
+ let tunnelIndex = tableView.clickedRow
+ guard tunnelIndex >= 0 && tunnelIndex < tunnelsManager.numberOfTunnels() else { return }
+ let tunnel = tunnelsManager.tunnel(at: tunnelIndex)
+ if tunnel.status == .inactive {
+ tunnelsManager.startActivation(of: tunnel)
+ } else if tunnel.status == .active {
+ tunnelsManager.startDeactivation(of: tunnel)
+ }
+ }
+
+ @discardableResult
+ private func selectTunnel(at index: Int) -> Bool {
+ if index < tunnelsManager.numberOfTunnels() {
+ tableView.scrollRowToVisible(index)
+ tableView.selectRowIndexes(IndexSet(integer: index), byExtendingSelection: false)
+ return true
+ }
+ return false
+ }
+}
+
+extension TunnelsListTableViewController: TunnelEditViewControllerDelegate {
+ func tunnelSaved(tunnel: TunnelContainer) {
+ if let tunnelIndex = tunnelsManager.index(of: tunnel), tunnelIndex >= 0 {
+ self.selectTunnel(at: tunnelIndex)
+ }
+ }
+
+ func tunnelEditingCancelled() {
+ // Nothing to do
+ }
+}
+
+extension TunnelsListTableViewController {
+ func tunnelAdded(at index: Int) {
+ tableView.insertRows(at: IndexSet(integer: index), withAnimation: .slideLeft)
+ if tunnelsManager.numberOfTunnels() == 1 {
+ selectTunnel(at: 0)
+ }
+ if !NSApp.isActive {
+ // macOS's VPN prompt might have caused us to lose focus
+ NSApp.activate(ignoringOtherApps: true)
+ }
+ }
+
+ func tunnelModified(at index: Int) {
+ tableView.reloadData(forRowIndexes: IndexSet(integer: index), columnIndexes: IndexSet(integer: 0))
+ }
+
+ func tunnelMoved(from oldIndex: Int, to newIndex: Int) {
+ tableView.moveRow(at: oldIndex, to: newIndex)
+ }
+
+ func tunnelRemoved(at index: Int) {
+ let selectedIndices = tableView.selectedRowIndexes
+ let isSingleSelectedTunnelBeingRemoved = selectedIndices.contains(index) && selectedIndices.count == 1
+ tableView.removeRows(at: IndexSet(integer: index), withAnimation: .slideLeft)
+ if tunnelsManager.numberOfTunnels() == 0 {
+ delegate?.tunnelsListEmpty()
+ } else if !isRemovingTunnelsFromWithinTheApp && isSingleSelectedTunnelBeingRemoved {
+ let newSelection = min(index, tunnelsManager.numberOfTunnels() - 1)
+ tableView.selectRowIndexes(IndexSet(integer: newSelection), byExtendingSelection: false)
+ }
+ }
+}
+
+extension TunnelsListTableViewController: NSTableViewDataSource {
+ func numberOfRows(in tableView: NSTableView) -> Int {
+ return tunnelsManager.numberOfTunnels()
+ }
+}
+
+extension TunnelsListTableViewController: NSTableViewDelegate {
+ func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
+ let cell: TunnelListRow = tableView.dequeueReusableCell()
+ cell.tunnel = tunnelsManager.tunnel(at: row)
+ return cell
+ }
+
+ func tableViewSelectionDidChange(_ notification: Notification) {
+ let selectedTunnelIndices = tableView.selectedRowIndexes.sorted()
+ if !selectedTunnelIndices.isEmpty {
+ delegate?.tunnelsSelected(tunnelIndices: tableView.selectedRowIndexes.sorted())
+ }
+ }
+}
+
+extension TunnelsListTableViewController {
+ override func keyDown(with event: NSEvent) {
+ if event.specialKey == .delete {
+ handleRemoveTunnelAction()
+ }
+ }
+}
+
+extension TunnelsListTableViewController: NSMenuItemValidation {
+ func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
+ if menuItem.action == #selector(TunnelsListTableViewController.handleRemoveTunnelAction) {
+ return !tableView.selectedRowIndexes.isEmpty
+ }
+ return true
+ }
+}
+
+class FillerButton: NSButton {
+ override var intrinsicContentSize: NSSize {
+ return NSSize(width: NSView.noIntrinsicMetric, height: NSView.noIntrinsicMetric)
+ }
+
+ init() {
+ super.init(frame: CGRect.zero)
+ title = ""
+ bezelStyle = .smallSquare
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func mouseDown(with event: NSEvent) {
+ // Eat mouseDown event, so that the button looks enabled but is unresponsive
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/ViewController/UnusableTunnelDetailViewController.swift b/Sources/WireGuardApp/UI/macOS/ViewController/UnusableTunnelDetailViewController.swift
new file mode 100644
index 0000000..612e8c1
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/ViewController/UnusableTunnelDetailViewController.swift
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Cocoa
+
+class UnusableTunnelDetailViewController: NSViewController {
+
+ var onButtonClicked: (() -> Void)?
+
+ let messageLabel: NSTextField = {
+ let text = tr("macUnusableTunnelMessage")
+ let boldFont = NSFont.boldSystemFont(ofSize: NSFont.systemFontSize)
+ let boldText = NSAttributedString(string: text, attributes: [.font: boldFont])
+ let label = NSTextField(labelWithAttributedString: boldText)
+ return label
+ }()
+
+ let infoLabel: NSTextField = {
+ let label = NSTextField(wrappingLabelWithString: tr("macUnusableTunnelInfo"))
+ return label
+ }()
+
+ let button: NSButton = {
+ let button = NSButton()
+ button.title = tr("macUnusableTunnelButtonTitleDeleteTunnel")
+ button.setButtonType(.momentaryPushIn)
+ button.bezelStyle = .rounded
+ return button
+ }()
+
+ init() {
+ super.init(nibName: nil, bundle: nil)
+ }
+
+ required init?(coder: NSCoder) {
+ fatalError("init(coder:) has not been implemented")
+ }
+
+ override func loadView() {
+
+ button.target = self
+ button.action = #selector(buttonClicked)
+
+ let margin: CGFloat = 20
+ let internalSpacing: CGFloat = 20
+ let buttonSpacing: CGFloat = 30
+ let stackView = NSStackView(views: [messageLabel, infoLabel, button])
+ stackView.orientation = .vertical
+ stackView.edgeInsets = NSEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)
+ stackView.spacing = internalSpacing
+ stackView.setCustomSpacing(buttonSpacing, after: infoLabel)
+
+ let view = NSView()
+ view.addSubview(stackView)
+ stackView.translatesAutoresizingMaskIntoConstraints = false
+
+ NSLayoutConstraint.activate([
+ stackView.widthAnchor.constraint(equalToConstant: 360),
+ stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
+ view.widthAnchor.constraint(greaterThanOrEqualToConstant: 420),
+ view.heightAnchor.constraint(greaterThanOrEqualToConstant: 240)
+ ])
+
+ self.view = view
+ }
+
+ @objc func buttonClicked() {
+ onButtonClicked?()
+ }
+}
diff --git a/Sources/WireGuardApp/UI/macOS/WireGuard.entitlements b/Sources/WireGuardApp/UI/macOS/WireGuard.entitlements
new file mode 100644
index 0000000..a39ba80
--- /dev/null
+++ b/Sources/WireGuardApp/UI/macOS/WireGuard.entitlements
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-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>com.apple.developer.networking.networkextension</key>
+ <array>
+ <string>packet-tunnel-provider</string>
+ </array>
+ <key>com.apple.security.app-sandbox</key>
+ <true/>
+ <key>com.apple.security.application-groups</key>
+ <array>
+ <string>$(DEVELOPMENT_TEAM).group.$(APP_ID_MACOS)</string>
+ </array>
+ <key>com.apple.security.files.user-selected.read-write</key>
+ <true/>
+</dict>
+</plist>
diff --git a/Sources/WireGuardApp/WireGuard-Bridging-Header.h b/Sources/WireGuardApp/WireGuard-Bridging-Header.h
new file mode 100644
index 0000000..955412e
--- /dev/null
+++ b/Sources/WireGuardApp/WireGuard-Bridging-Header.h
@@ -0,0 +1,9 @@
+#include "unzip.h"
+#include "zip.h"
+#include "ringlogger.h"
+#include "highlighter.h"
+
+#import "TargetConditionals.h"
+#if TARGET_OS_OSX
+#include <libproc.h>
+#endif
diff --git a/Sources/WireGuardApp/WireGuardAppError.swift b/Sources/WireGuardApp/WireGuardAppError.swift
new file mode 100644
index 0000000..7acc62c
--- /dev/null
+++ b/Sources/WireGuardApp/WireGuardAppError.swift
@@ -0,0 +1,8 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+protocol WireGuardAppError: Error {
+ typealias AlertText = (title: String, message: String)
+
+ var alertText: AlertText { get }
+}
diff --git a/Sources/WireGuardApp/WireGuardResult.swift b/Sources/WireGuardApp/WireGuardResult.swift
new file mode 100644
index 0000000..e326f0c
--- /dev/null
+++ b/Sources/WireGuardApp/WireGuardResult.swift
@@ -0,0 +1,28 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+enum WireGuardResult<T> {
+ case success(_ value: T)
+ case failure(_ error: WireGuardAppError)
+
+ var value: T? {
+ switch self {
+ case .success(let value): return value
+ case .failure: return nil
+ }
+ }
+
+ var error: WireGuardAppError? {
+ switch self {
+ case .success: return nil
+ case .failure(let error): return error
+ }
+ }
+
+ var isSuccess: Bool {
+ switch self {
+ case .success: return true
+ case .failure: return false
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/MiniZip64_info.txt b/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/MiniZip64_info.txt
new file mode 100644
index 0000000..57d7152
--- /dev/null
+++ b/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/MiniZip64_info.txt
@@ -0,0 +1,74 @@
+MiniZip - Copyright (c) 1998-2010 - by Gilles Vollant - version 1.1 64 bits from Mathias Svensson
+
+Introduction
+---------------------
+MiniZip 1.1 is built from MiniZip 1.0 by Gilles Vollant ( http://www.winimage.com/zLibDll/minizip.html )
+
+When adding ZIP64 support into minizip it would result into risk of breaking compatibility with minizip 1.0.
+All possible work was done for compatibility.
+
+
+Background
+---------------------
+When adding ZIP64 support Mathias Svensson found that Even Rouault have added ZIP64
+support for unzip.c into minizip for a open source project called gdal ( http://www.gdal.org/ )
+
+That was used as a starting point. And after that ZIP64 support was added to zip.c
+some refactoring and code cleanup was also done.
+
+
+Changed from MiniZip 1.0 to MiniZip 1.1
+---------------------------------------
+* Added ZIP64 support for unzip ( by Even Rouault )
+* Added ZIP64 support for zip ( by Mathias Svensson )
+* Reverted some changed that Even Rouault did.
+* Bunch of patches received from Gulles Vollant that he received for MiniZip from various users.
+* Added unzip patch for BZIP Compression method (patch create by Daniel Borca)
+* Added BZIP Compress method for zip
+* Did some refactoring and code cleanup
+
+
+Credits
+
+ Gilles Vollant - Original MiniZip author
+ Even Rouault - ZIP64 unzip Support
+ Daniel Borca - BZip Compression method support in unzip
+ Mathias Svensson - ZIP64 zip support
+ Mathias Svensson - BZip Compression method support in zip
+
+ Resources
+
+ ZipLayout http://result42.com/projects/ZipFileLayout
+ Command line tool for Windows that shows the layout and information of the headers in a zip archive.
+ Used when debugging and validating the creation of zip files using MiniZip64
+
+
+ ZIP App Note http://www.pkware.com/documents/casestudies/APPNOTE.TXT
+ Zip File specification
+
+
+Notes.
+ * To be able to use BZip compression method in zip64.c or unzip64.c the BZIP2 lib is needed and HAVE_BZIP2 need to be defined.
+
+License
+----------------------------------------------------------
+ Condition of use and distribution are the same than zlib :
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+
+----------------------------------------------------------
+
diff --git a/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/ioapi.c b/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/ioapi.c
new file mode 100644
index 0000000..7f5c191
--- /dev/null
+++ b/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/ioapi.c
@@ -0,0 +1,247 @@
+/* ioapi.h -- IO base function header for compress/uncompress .zip
+ part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+ Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+ Modifications for Zip64 support
+ Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+ For more info read MiniZip_info.txt
+
+*/
+
+#if defined(_WIN32) && (!(defined(_CRT_SECURE_NO_WARNINGS)))
+ #define _CRT_SECURE_NO_WARNINGS
+#endif
+
+#if defined(__APPLE__) || defined(IOAPI_NO_64)
+// In darwin and perhaps other BSD variants off_t is a 64 bit value, hence no need for specific 64 bit functions
+#define FOPEN_FUNC(filename, mode) fopen(filename, mode)
+#define FTELLO_FUNC(stream) ftello(stream)
+#define FSEEKO_FUNC(stream, offset, origin) fseeko(stream, offset, origin)
+#else
+#define FOPEN_FUNC(filename, mode) fopen64(filename, mode)
+#define FTELLO_FUNC(stream) ftello64(stream)
+#define FSEEKO_FUNC(stream, offset, origin) fseeko64(stream, offset, origin)
+#endif
+
+
+#include "ioapi.h"
+
+voidpf call_zopen64 (const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode)
+{
+ if (pfilefunc->zfile_func64.zopen64_file != NULL)
+ return (*(pfilefunc->zfile_func64.zopen64_file)) (pfilefunc->zfile_func64.opaque,filename,mode);
+ else
+ {
+ return (*(pfilefunc->zopen32_file))(pfilefunc->zfile_func64.opaque,(const char*)filename,mode);
+ }
+}
+
+long call_zseek64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin)
+{
+ if (pfilefunc->zfile_func64.zseek64_file != NULL)
+ return (*(pfilefunc->zfile_func64.zseek64_file)) (pfilefunc->zfile_func64.opaque,filestream,offset,origin);
+ else
+ {
+ uLong offsetTruncated = (uLong)offset;
+ if (offsetTruncated != offset)
+ return -1;
+ else
+ return (*(pfilefunc->zseek32_file))(pfilefunc->zfile_func64.opaque,filestream,offsetTruncated,origin);
+ }
+}
+
+ZPOS64_T call_ztell64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream)
+{
+ if (pfilefunc->zfile_func64.zseek64_file != NULL)
+ return (*(pfilefunc->zfile_func64.ztell64_file)) (pfilefunc->zfile_func64.opaque,filestream);
+ else
+ {
+ uLong tell_uLong = (*(pfilefunc->ztell32_file))(pfilefunc->zfile_func64.opaque,filestream);
+ if ((tell_uLong) == MAXU32)
+ return (ZPOS64_T)-1;
+ else
+ return tell_uLong;
+ }
+}
+
+void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32)
+{
+ p_filefunc64_32->zfile_func64.zopen64_file = NULL;
+ p_filefunc64_32->zopen32_file = p_filefunc32->zopen_file;
+ p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file;
+ p_filefunc64_32->zfile_func64.zread_file = p_filefunc32->zread_file;
+ p_filefunc64_32->zfile_func64.zwrite_file = p_filefunc32->zwrite_file;
+ p_filefunc64_32->zfile_func64.ztell64_file = NULL;
+ p_filefunc64_32->zfile_func64.zseek64_file = NULL;
+ p_filefunc64_32->zfile_func64.zclose_file = p_filefunc32->zclose_file;
+ p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file;
+ p_filefunc64_32->zfile_func64.opaque = p_filefunc32->opaque;
+ p_filefunc64_32->zseek32_file = p_filefunc32->zseek_file;
+ p_filefunc64_32->ztell32_file = p_filefunc32->ztell_file;
+}
+
+
+
+static voidpf ZCALLBACK fopen_file_func OF((voidpf opaque, const char* filename, int mode));
+static uLong ZCALLBACK fread_file_func OF((voidpf opaque, voidpf stream, void* buf, uLong size));
+static uLong ZCALLBACK fwrite_file_func OF((voidpf opaque, voidpf stream, const void* buf,uLong size));
+static ZPOS64_T ZCALLBACK ftell64_file_func OF((voidpf opaque, voidpf stream));
+static long ZCALLBACK fseek64_file_func OF((voidpf opaque, voidpf stream, ZPOS64_T offset, int origin));
+static int ZCALLBACK fclose_file_func OF((voidpf opaque, voidpf stream));
+static int ZCALLBACK ferror_file_func OF((voidpf opaque, voidpf stream));
+
+static voidpf ZCALLBACK fopen_file_func (voidpf opaque, const char* filename, int mode)
+{
+ FILE* file = NULL;
+ const char* mode_fopen = NULL;
+ if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ)
+ mode_fopen = "rb";
+ else
+ if (mode & ZLIB_FILEFUNC_MODE_EXISTING)
+ mode_fopen = "r+b";
+ else
+ if (mode & ZLIB_FILEFUNC_MODE_CREATE)
+ mode_fopen = "wb";
+
+ if ((filename!=NULL) && (mode_fopen != NULL))
+ file = fopen(filename, mode_fopen);
+ return file;
+}
+
+static voidpf ZCALLBACK fopen64_file_func (voidpf opaque, const void* filename, int mode)
+{
+ FILE* file = NULL;
+ const char* mode_fopen = NULL;
+ if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ)
+ mode_fopen = "rb";
+ else
+ if (mode & ZLIB_FILEFUNC_MODE_EXISTING)
+ mode_fopen = "r+b";
+ else
+ if (mode & ZLIB_FILEFUNC_MODE_CREATE)
+ mode_fopen = "wb";
+
+ if ((filename!=NULL) && (mode_fopen != NULL))
+ file = FOPEN_FUNC((const char*)filename, mode_fopen);
+ return file;
+}
+
+
+static uLong ZCALLBACK fread_file_func (voidpf opaque, voidpf stream, void* buf, uLong size)
+{
+ uLong ret;
+ ret = (uLong)fread(buf, 1, (size_t)size, (FILE *)stream);
+ return ret;
+}
+
+static uLong ZCALLBACK fwrite_file_func (voidpf opaque, voidpf stream, const void* buf, uLong size)
+{
+ uLong ret;
+ ret = (uLong)fwrite(buf, 1, (size_t)size, (FILE *)stream);
+ return ret;
+}
+
+static long ZCALLBACK ftell_file_func (voidpf opaque, voidpf stream)
+{
+ long ret;
+ ret = ftell((FILE *)stream);
+ return ret;
+}
+
+
+static ZPOS64_T ZCALLBACK ftell64_file_func (voidpf opaque, voidpf stream)
+{
+ ZPOS64_T ret;
+ ret = FTELLO_FUNC((FILE *)stream);
+ return ret;
+}
+
+static long ZCALLBACK fseek_file_func (voidpf opaque, voidpf stream, uLong offset, int origin)
+{
+ int fseek_origin=0;
+ long ret;
+ switch (origin)
+ {
+ case ZLIB_FILEFUNC_SEEK_CUR :
+ fseek_origin = SEEK_CUR;
+ break;
+ case ZLIB_FILEFUNC_SEEK_END :
+ fseek_origin = SEEK_END;
+ break;
+ case ZLIB_FILEFUNC_SEEK_SET :
+ fseek_origin = SEEK_SET;
+ break;
+ default: return -1;
+ }
+ ret = 0;
+ if (fseek((FILE *)stream, offset, fseek_origin) != 0)
+ ret = -1;
+ return ret;
+}
+
+static long ZCALLBACK fseek64_file_func (voidpf opaque, voidpf stream, ZPOS64_T offset, int origin)
+{
+ int fseek_origin=0;
+ long ret;
+ switch (origin)
+ {
+ case ZLIB_FILEFUNC_SEEK_CUR :
+ fseek_origin = SEEK_CUR;
+ break;
+ case ZLIB_FILEFUNC_SEEK_END :
+ fseek_origin = SEEK_END;
+ break;
+ case ZLIB_FILEFUNC_SEEK_SET :
+ fseek_origin = SEEK_SET;
+ break;
+ default: return -1;
+ }
+ ret = 0;
+
+ if(FSEEKO_FUNC((FILE *)stream, offset, fseek_origin) != 0)
+ ret = -1;
+
+ return ret;
+}
+
+
+static int ZCALLBACK fclose_file_func (voidpf opaque, voidpf stream)
+{
+ int ret;
+ ret = fclose((FILE *)stream);
+ return ret;
+}
+
+static int ZCALLBACK ferror_file_func (voidpf opaque, voidpf stream)
+{
+ int ret;
+ ret = ferror((FILE *)stream);
+ return ret;
+}
+
+void fill_fopen_filefunc (pzlib_filefunc_def)
+ zlib_filefunc_def* pzlib_filefunc_def;
+{
+ pzlib_filefunc_def->zopen_file = fopen_file_func;
+ pzlib_filefunc_def->zread_file = fread_file_func;
+ pzlib_filefunc_def->zwrite_file = fwrite_file_func;
+ pzlib_filefunc_def->ztell_file = ftell_file_func;
+ pzlib_filefunc_def->zseek_file = fseek_file_func;
+ pzlib_filefunc_def->zclose_file = fclose_file_func;
+ pzlib_filefunc_def->zerror_file = ferror_file_func;
+ pzlib_filefunc_def->opaque = NULL;
+}
+
+void fill_fopen64_filefunc (zlib_filefunc64_def* pzlib_filefunc_def)
+{
+ pzlib_filefunc_def->zopen64_file = fopen64_file_func;
+ pzlib_filefunc_def->zread_file = fread_file_func;
+ pzlib_filefunc_def->zwrite_file = fwrite_file_func;
+ pzlib_filefunc_def->ztell64_file = ftell64_file_func;
+ pzlib_filefunc_def->zseek64_file = fseek64_file_func;
+ pzlib_filefunc_def->zclose_file = fclose_file_func;
+ pzlib_filefunc_def->zerror_file = ferror_file_func;
+ pzlib_filefunc_def->opaque = NULL;
+}
diff --git a/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/ioapi.h b/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/ioapi.h
new file mode 100644
index 0000000..8dcbdb0
--- /dev/null
+++ b/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/ioapi.h
@@ -0,0 +1,208 @@
+/* ioapi.h -- IO base function header for compress/uncompress .zip
+ part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+ Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+ Modifications for Zip64 support
+ Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+ For more info read MiniZip_info.txt
+
+ Changes
+
+ Oct-2009 - Defined ZPOS64_T to fpos_t on windows and u_int64_t on linux. (might need to find a better why for this)
+ Oct-2009 - Change to fseeko64, ftello64 and fopen64 so large files would work on linux.
+ More if/def section may be needed to support other platforms
+ Oct-2009 - Defined fxxxx64 calls to normal fopen/ftell/fseek so they would compile on windows.
+ (but you should use iowin32.c for windows instead)
+
+*/
+
+#ifndef _ZLIBIOAPI64_H
+#define _ZLIBIOAPI64_H
+
+#if (!defined(_WIN32)) && (!defined(WIN32)) && (!defined(__APPLE__))
+
+ // Linux needs this to support file operation on files larger then 4+GB
+ // But might need better if/def to select just the platforms that needs them.
+
+ #ifndef __USE_FILE_OFFSET64
+ #define __USE_FILE_OFFSET64
+ #endif
+ #ifndef __USE_LARGEFILE64
+ #define __USE_LARGEFILE64
+ #endif
+ #ifndef _LARGEFILE64_SOURCE
+ #define _LARGEFILE64_SOURCE
+ #endif
+ #ifndef _FILE_OFFSET_BIT
+ #define _FILE_OFFSET_BIT 64
+ #endif
+
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include "zlib.h"
+
+#if defined(USE_FILE32API)
+#define fopen64 fopen
+#define ftello64 ftell
+#define fseeko64 fseek
+#else
+#ifdef __FreeBSD__
+#define fopen64 fopen
+#define ftello64 ftello
+#define fseeko64 fseeko
+#endif
+#ifdef _MSC_VER
+ #define fopen64 fopen
+ #if (_MSC_VER >= 1400) && (!(defined(NO_MSCVER_FILE64_FUNC)))
+ #define ftello64 _ftelli64
+ #define fseeko64 _fseeki64
+ #else // old MSC
+ #define ftello64 ftell
+ #define fseeko64 fseek
+ #endif
+#endif
+#endif
+
+/*
+#ifndef ZPOS64_T
+ #ifdef _WIN32
+ #define ZPOS64_T fpos_t
+ #else
+ #include <stdint.h>
+ #define ZPOS64_T uint64_t
+ #endif
+#endif
+*/
+
+#ifdef HAVE_MINIZIP64_CONF_H
+#include "mz64conf.h"
+#endif
+
+/* a type choosen by DEFINE */
+#ifdef HAVE_64BIT_INT_CUSTOM
+typedef 64BIT_INT_CUSTOM_TYPE ZPOS64_T;
+#else
+#ifdef HAS_STDINT_H
+#include "stdint.h"
+typedef uint64_t ZPOS64_T;
+#else
+
+/* Maximum unsigned 32-bit value used as placeholder for zip64 */
+#define MAXU32 0xffffffff
+
+#if defined(_MSC_VER) || defined(__BORLANDC__)
+typedef unsigned __int64 ZPOS64_T;
+#else
+typedef unsigned long long int ZPOS64_T;
+#endif
+#endif
+#endif
+
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+
+#define ZLIB_FILEFUNC_SEEK_CUR (1)
+#define ZLIB_FILEFUNC_SEEK_END (2)
+#define ZLIB_FILEFUNC_SEEK_SET (0)
+
+#define ZLIB_FILEFUNC_MODE_READ (1)
+#define ZLIB_FILEFUNC_MODE_WRITE (2)
+#define ZLIB_FILEFUNC_MODE_READWRITEFILTER (3)
+
+#define ZLIB_FILEFUNC_MODE_EXISTING (4)
+#define ZLIB_FILEFUNC_MODE_CREATE (8)
+
+
+#ifndef ZCALLBACK
+ #if (defined(WIN32) || defined(_WIN32) || defined (WINDOWS) || defined (_WINDOWS)) && defined(CALLBACK) && defined (USEWINDOWS_CALLBACK)
+ #define ZCALLBACK CALLBACK
+ #else
+ #define ZCALLBACK
+ #endif
+#endif
+
+
+
+
+typedef voidpf (ZCALLBACK *open_file_func) OF((voidpf opaque, const char* filename, int mode));
+typedef uLong (ZCALLBACK *read_file_func) OF((voidpf opaque, voidpf stream, void* buf, uLong size));
+typedef uLong (ZCALLBACK *write_file_func) OF((voidpf opaque, voidpf stream, const void* buf, uLong size));
+typedef int (ZCALLBACK *close_file_func) OF((voidpf opaque, voidpf stream));
+typedef int (ZCALLBACK *testerror_file_func) OF((voidpf opaque, voidpf stream));
+
+typedef long (ZCALLBACK *tell_file_func) OF((voidpf opaque, voidpf stream));
+typedef long (ZCALLBACK *seek_file_func) OF((voidpf opaque, voidpf stream, uLong offset, int origin));
+
+
+/* here is the "old" 32 bits structure structure */
+typedef struct zlib_filefunc_def_s
+{
+ open_file_func zopen_file;
+ read_file_func zread_file;
+ write_file_func zwrite_file;
+ tell_file_func ztell_file;
+ seek_file_func zseek_file;
+ close_file_func zclose_file;
+ testerror_file_func zerror_file;
+ voidpf opaque;
+} zlib_filefunc_def;
+
+typedef ZPOS64_T (ZCALLBACK *tell64_file_func) OF((voidpf opaque, voidpf stream));
+typedef long (ZCALLBACK *seek64_file_func) OF((voidpf opaque, voidpf stream, ZPOS64_T offset, int origin));
+typedef voidpf (ZCALLBACK *open64_file_func) OF((voidpf opaque, const void* filename, int mode));
+
+typedef struct zlib_filefunc64_def_s
+{
+ open64_file_func zopen64_file;
+ read_file_func zread_file;
+ write_file_func zwrite_file;
+ tell64_file_func ztell64_file;
+ seek64_file_func zseek64_file;
+ close_file_func zclose_file;
+ testerror_file_func zerror_file;
+ voidpf opaque;
+} zlib_filefunc64_def;
+
+void fill_fopen64_filefunc OF((zlib_filefunc64_def* pzlib_filefunc_def));
+void fill_fopen_filefunc OF((zlib_filefunc_def* pzlib_filefunc_def));
+
+/* now internal definition, only for zip.c and unzip.h */
+typedef struct zlib_filefunc64_32_def_s
+{
+ zlib_filefunc64_def zfile_func64;
+ open_file_func zopen32_file;
+ tell_file_func ztell32_file;
+ seek_file_func zseek32_file;
+} zlib_filefunc64_32_def;
+
+
+#define ZREAD64(filefunc,filestream,buf,size) ((*((filefunc).zfile_func64.zread_file)) ((filefunc).zfile_func64.opaque,filestream,buf,size))
+#define ZWRITE64(filefunc,filestream,buf,size) ((*((filefunc).zfile_func64.zwrite_file)) ((filefunc).zfile_func64.opaque,filestream,buf,size))
+//#define ZTELL64(filefunc,filestream) ((*((filefunc).ztell64_file)) ((filefunc).opaque,filestream))
+//#define ZSEEK64(filefunc,filestream,pos,mode) ((*((filefunc).zseek64_file)) ((filefunc).opaque,filestream,pos,mode))
+#define ZCLOSE64(filefunc,filestream) ((*((filefunc).zfile_func64.zclose_file)) ((filefunc).zfile_func64.opaque,filestream))
+#define ZERROR64(filefunc,filestream) ((*((filefunc).zfile_func64.zerror_file)) ((filefunc).zfile_func64.opaque,filestream))
+
+voidpf call_zopen64 OF((const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode));
+long call_zseek64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin));
+ZPOS64_T call_ztell64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf filestream));
+
+void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32);
+
+#define ZOPEN64(filefunc,filename,mode) (call_zopen64((&(filefunc)),(filename),(mode)))
+#define ZTELL64(filefunc,filestream) (call_ztell64((&(filefunc)),(filestream)))
+#define ZSEEK64(filefunc,filestream,pos,mode) (call_zseek64((&(filefunc)),(filestream),(pos),(mode)))
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/unzip.c b/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/unzip.c
new file mode 100644
index 0000000..bdd18d8
--- /dev/null
+++ b/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/unzip.c
@@ -0,0 +1,2071 @@
+/* unzip.c -- IO for uncompress .zip files using zlib
+ Version 1.1, February 14h, 2010
+ part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+ Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+ Modifications of Unzip for Zip64
+ Copyright (C) 2007-2008 Even Rouault
+
+ Modifications for Zip64 support on both zip and unzip
+ Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+ For more info read MiniZip_info.txt
+
+
+ ------------------------------------------------------------------------------------
+ Decryption code comes from crypt.c by Info-ZIP but has been greatly reduced in terms of
+ compatibility with older software. The following is from the original crypt.c.
+ Code woven in by Terry Thorsen 1/2003.
+
+ Copyright (c) 1990-2000 Info-ZIP. All rights reserved.
+
+ See the accompanying file LICENSE, version 2000-Apr-09 or later
+ (the contents of which are also included in zip.h) for terms of use.
+ If, for some reason, all these files are missing, the Info-ZIP license
+ also may be found at: ftp://ftp.info-zip.org/pub/infozip/license.html
+
+ crypt.c (full version) by Info-ZIP. Last revised: [see crypt.h]
+
+ The encryption/decryption parts of this source code (as opposed to the
+ non-echoing password parts) were originally written in Europe. The
+ whole source package can be freely distributed, including from the USA.
+ (Prior to January 2000, re-export from the US was a violation of US law.)
+
+ This encryption code is a direct transcription of the algorithm from
+ Roger Schlafly, described by Phil Katz in the file appnote.txt. This
+ file (appnote.txt) is distributed with the PKZIP program (even in the
+ version without encryption capabilities).
+
+ ------------------------------------------------------------------------------------
+
+ Changes in unzip.c
+
+ 2007-2008 - Even Rouault - Addition of cpl_unzGetCurrentFileZStreamPos
+ 2007-2008 - Even Rouault - Decoration of symbol names unz* -> cpl_unz*
+ 2007-2008 - Even Rouault - Remove old C style function prototypes
+ 2007-2008 - Even Rouault - Add unzip support for ZIP64
+
+ Copyright (C) 2007-2008 Even Rouault
+
+
+ Oct-2009 - Mathias Svensson - Removed cpl_* from symbol names (Even Rouault added them but since this is now moved to a new project (minizip64) I renamed them again).
+ Oct-2009 - Mathias Svensson - Fixed problem if uncompressed size was > 4G and compressed size was <4G
+ should only read the compressed/uncompressed size from the Zip64 format if
+ the size from normal header was 0xFFFFFFFF
+ Oct-2009 - Mathias Svensson - Applied some bug fixes from paches recived from Gilles Vollant
+ Oct-2009 - Mathias Svensson - Applied support to unzip files with compression mathod BZIP2 (bzip2 lib is required)
+ Patch created by Daniel Borca
+
+ Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer
+
+ Copyright (C) 1998 - 2010 Gilles Vollant, Even Rouault, Mathias Svensson
+
+*/
+
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "zlib.h"
+#include "unzip.h"
+
+#ifdef STDC
+# include <stddef.h>
+# include <string.h>
+# include <stdlib.h>
+#endif
+#ifdef NO_ERRNO_H
+ extern int errno;
+#else
+# include <errno.h>
+#endif
+
+
+#ifndef local
+# define local static
+#endif
+/* compile with -Dlocal if your debugger can't find static symbols */
+
+
+#ifndef CASESENSITIVITYDEFAULT_NO
+# if !defined(unix) && !defined(CASESENSITIVITYDEFAULT_YES)
+# define CASESENSITIVITYDEFAULT_NO
+# endif
+#endif
+
+
+#ifndef UNZ_BUFSIZE
+#define UNZ_BUFSIZE (16384)
+#endif
+
+#ifndef UNZ_MAXFILENAMEINZIP
+#define UNZ_MAXFILENAMEINZIP (256)
+#endif
+
+#ifndef ALLOC
+# define ALLOC(size) (malloc(size))
+#endif
+#ifndef TRYFREE
+# define TRYFREE(p) {if (p) free(p);}
+#endif
+
+#define SIZECENTRALDIRITEM (0x2e)
+#define SIZEZIPLOCALHEADER (0x1e)
+
+
+const char unz_copyright[] =
+ " unzip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll";
+
+/* unz_file_info_interntal contain internal info about a file in zipfile*/
+typedef struct unz_file_info64_internal_s
+{
+ ZPOS64_T offset_curfile;/* relative offset of local header 8 bytes */
+} unz_file_info64_internal;
+
+
+/* file_in_zip_read_info_s contain internal information about a file in zipfile,
+ when reading and decompress it */
+typedef struct
+{
+ char *read_buffer; /* internal buffer for compressed data */
+ z_stream stream; /* zLib stream structure for inflate */
+
+#ifdef HAVE_BZIP2
+ bz_stream bstream; /* bzLib stream structure for bziped */
+#endif
+
+ ZPOS64_T pos_in_zipfile; /* position in byte on the zipfile, for fseek*/
+ uLong stream_initialised; /* flag set if stream structure is initialised*/
+
+ ZPOS64_T offset_local_extrafield;/* offset of the local extra field */
+ uInt size_local_extrafield;/* size of the local extra field */
+ ZPOS64_T pos_local_extrafield; /* position in the local extra field in read*/
+ ZPOS64_T total_out_64;
+
+ uLong crc32; /* crc32 of all data uncompressed */
+ uLong crc32_wait; /* crc32 we must obtain after decompress all */
+ ZPOS64_T rest_read_compressed; /* number of byte to be decompressed */
+ ZPOS64_T rest_read_uncompressed;/*number of byte to be obtained after decomp*/
+ zlib_filefunc64_32_def z_filefunc;
+ voidpf filestream; /* io structore of the zipfile */
+ uLong compression_method; /* compression method (0==store) */
+ ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/
+ int raw;
+} file_in_zip64_read_info_s;
+
+
+/* unz64_s contain internal information about the zipfile
+*/
+typedef struct
+{
+ zlib_filefunc64_32_def z_filefunc;
+ int is64bitOpenFunction;
+ voidpf filestream; /* io structore of the zipfile */
+ unz_global_info64 gi; /* public global information */
+ ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/
+ ZPOS64_T num_file; /* number of the current file in the zipfile*/
+ ZPOS64_T pos_in_central_dir; /* pos of the current file in the central dir*/
+ ZPOS64_T current_file_ok; /* flag about the usability of the current file*/
+ ZPOS64_T central_pos; /* position of the beginning of the central dir*/
+
+ ZPOS64_T size_central_dir; /* size of the central directory */
+ ZPOS64_T offset_central_dir; /* offset of start of central directory with
+ respect to the starting disk number */
+
+ unz_file_info64 cur_file_info; /* public info about the current file in zip*/
+ unz_file_info64_internal cur_file_info_internal; /* private info about it*/
+ file_in_zip64_read_info_s* pfile_in_zip_read; /* structure about the current
+ file if we are decompressing it */
+ int encrypted;
+
+ int isZip64;
+} unz64_s;
+
+/* ===========================================================================
+ Read a byte from a gz_stream; update next_in and avail_in. Return EOF
+ for end of file.
+ IN assertion: the stream s has been successfully opened for reading.
+*/
+
+
+local int unz64local_getByte OF((
+ const zlib_filefunc64_32_def* pzlib_filefunc_def,
+ voidpf filestream,
+ int *pi));
+
+local int unz64local_getByte(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, int *pi)
+{
+ unsigned char c;
+ int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,&c,1);
+ if (err==1)
+ {
+ *pi = (int)c;
+ return UNZ_OK;
+ }
+ else
+ {
+ if (ZERROR64(*pzlib_filefunc_def,filestream))
+ return UNZ_ERRNO;
+ else
+ return UNZ_EOF;
+ }
+}
+
+
+/* ===========================================================================
+ Reads a long in LSB order from the given gz_stream. Sets
+*/
+local int unz64local_getShort OF((
+ const zlib_filefunc64_32_def* pzlib_filefunc_def,
+ voidpf filestream,
+ uLong *pX));
+
+local int unz64local_getShort (const zlib_filefunc64_32_def* pzlib_filefunc_def,
+ voidpf filestream,
+ uLong *pX)
+{
+ uLong x ;
+ int i = 0;
+ int err;
+
+ err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x = (uLong)i;
+
+ if (err==UNZ_OK)
+ err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x |= ((uLong)i)<<8;
+
+ if (err==UNZ_OK)
+ *pX = x;
+ else
+ *pX = 0;
+ return err;
+}
+
+local int unz64local_getLong OF((
+ const zlib_filefunc64_32_def* pzlib_filefunc_def,
+ voidpf filestream,
+ uLong *pX));
+
+local int unz64local_getLong (const zlib_filefunc64_32_def* pzlib_filefunc_def,
+ voidpf filestream,
+ uLong *pX)
+{
+ uLong x ;
+ int i = 0;
+ int err;
+
+ err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x = (uLong)i;
+
+ if (err==UNZ_OK)
+ err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x |= ((uLong)i)<<8;
+
+ if (err==UNZ_OK)
+ err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x |= ((uLong)i)<<16;
+
+ if (err==UNZ_OK)
+ err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x += ((uLong)i)<<24;
+
+ if (err==UNZ_OK)
+ *pX = x;
+ else
+ *pX = 0;
+ return err;
+}
+
+local int unz64local_getLong64 OF((
+ const zlib_filefunc64_32_def* pzlib_filefunc_def,
+ voidpf filestream,
+ ZPOS64_T *pX));
+
+
+local int unz64local_getLong64 (const zlib_filefunc64_32_def* pzlib_filefunc_def,
+ voidpf filestream,
+ ZPOS64_T *pX)
+{
+ ZPOS64_T x ;
+ int i = 0;
+ int err;
+
+ err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x = (ZPOS64_T)i;
+
+ if (err==UNZ_OK)
+ err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x |= ((ZPOS64_T)i)<<8;
+
+ if (err==UNZ_OK)
+ err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x |= ((ZPOS64_T)i)<<16;
+
+ if (err==UNZ_OK)
+ err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x |= ((ZPOS64_T)i)<<24;
+
+ if (err==UNZ_OK)
+ err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x |= ((ZPOS64_T)i)<<32;
+
+ if (err==UNZ_OK)
+ err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x |= ((ZPOS64_T)i)<<40;
+
+ if (err==UNZ_OK)
+ err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x |= ((ZPOS64_T)i)<<48;
+
+ if (err==UNZ_OK)
+ err = unz64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x |= ((ZPOS64_T)i)<<56;
+
+ if (err==UNZ_OK)
+ *pX = x;
+ else
+ *pX = 0;
+ return err;
+}
+
+/* My own strcmpi / strcasecmp */
+local int strcmpcasenosensitive_internal (const char* fileName1, const char* fileName2)
+{
+ for (;;)
+ {
+ char c1=*(fileName1++);
+ char c2=*(fileName2++);
+ if ((c1>='a') && (c1<='z'))
+ c1 -= 0x20;
+ if ((c2>='a') && (c2<='z'))
+ c2 -= 0x20;
+ if (c1=='\0')
+ return ((c2=='\0') ? 0 : -1);
+ if (c2=='\0')
+ return 1;
+ if (c1<c2)
+ return -1;
+ if (c1>c2)
+ return 1;
+ }
+}
+
+
+#ifdef CASESENSITIVITYDEFAULT_NO
+#define CASESENSITIVITYDEFAULTVALUE 2
+#else
+#define CASESENSITIVITYDEFAULTVALUE 1
+#endif
+
+#ifndef STRCMPCASENOSENTIVEFUNCTION
+#define STRCMPCASENOSENTIVEFUNCTION strcmpcasenosensitive_internal
+#endif
+
+/*
+ Compare two filename (fileName1,fileName2).
+ If iCaseSenisivity = 1, comparision is case sensitivity (like strcmp)
+ If iCaseSenisivity = 2, comparision is not case sensitivity (like strcmpi
+ or strcasecmp)
+ If iCaseSenisivity = 0, case sensitivity is defaut of your operating system
+ (like 1 on Unix, 2 on Windows)
+
+*/
+extern int ZEXPORT unzStringFileNameCompare (const char* fileName1,
+ const char* fileName2,
+ int iCaseSensitivity)
+
+{
+ if (iCaseSensitivity==0)
+ iCaseSensitivity=CASESENSITIVITYDEFAULTVALUE;
+
+ if (iCaseSensitivity==1)
+ return strcmp(fileName1,fileName2);
+
+ return STRCMPCASENOSENTIVEFUNCTION(fileName1,fileName2);
+}
+
+#ifndef BUFREADCOMMENT
+#define BUFREADCOMMENT (0x400)
+#endif
+
+/*
+ Locate the Central directory of a zipfile (at the end, just before
+ the global comment)
+*/
+local ZPOS64_T unz64local_SearchCentralDir OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream));
+local ZPOS64_T unz64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream)
+{
+ unsigned char* buf;
+ ZPOS64_T uSizeFile;
+ ZPOS64_T uBackRead;
+ ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */
+ ZPOS64_T uPosFound=0;
+
+ if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)
+ return 0;
+
+
+ uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);
+
+ if (uMaxBack>uSizeFile)
+ uMaxBack = uSizeFile;
+
+ buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);
+ if (buf==NULL)
+ return 0;
+
+ uBackRead = 4;
+ while (uBackRead<uMaxBack)
+ {
+ uLong uReadSize;
+ ZPOS64_T uReadPos ;
+ int i;
+ if (uBackRead+BUFREADCOMMENT>uMaxBack)
+ uBackRead = uMaxBack;
+ else
+ uBackRead+=BUFREADCOMMENT;
+ uReadPos = uSizeFile-uBackRead ;
+
+ uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?
+ (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);
+ if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ break;
+
+ if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)
+ break;
+
+ for (i=(int)uReadSize-3; (i--)>0;)
+ if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) &&
+ ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06))
+ {
+ uPosFound = uReadPos+i;
+ break;
+ }
+
+ if (uPosFound!=0)
+ break;
+ }
+ TRYFREE(buf);
+ return uPosFound;
+}
+
+
+/*
+ Locate the Central directory 64 of a zipfile (at the end, just before
+ the global comment)
+*/
+local ZPOS64_T unz64local_SearchCentralDir64 OF((
+ const zlib_filefunc64_32_def* pzlib_filefunc_def,
+ voidpf filestream));
+
+local ZPOS64_T unz64local_SearchCentralDir64(const zlib_filefunc64_32_def* pzlib_filefunc_def,
+ voidpf filestream)
+{
+ unsigned char* buf;
+ ZPOS64_T uSizeFile;
+ ZPOS64_T uBackRead;
+ ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */
+ ZPOS64_T uPosFound=0;
+ uLong uL;
+ ZPOS64_T relativeOffset;
+
+ if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)
+ return 0;
+
+
+ uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);
+
+ if (uMaxBack>uSizeFile)
+ uMaxBack = uSizeFile;
+
+ buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);
+ if (buf==NULL)
+ return 0;
+
+ uBackRead = 4;
+ while (uBackRead<uMaxBack)
+ {
+ uLong uReadSize;
+ ZPOS64_T uReadPos;
+ int i;
+ if (uBackRead+BUFREADCOMMENT>uMaxBack)
+ uBackRead = uMaxBack;
+ else
+ uBackRead+=BUFREADCOMMENT;
+ uReadPos = uSizeFile-uBackRead ;
+
+ uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?
+ (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);
+ if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ break;
+
+ if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)
+ break;
+
+ for (i=(int)uReadSize-3; (i--)>0;)
+ if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) &&
+ ((*(buf+i+2))==0x06) && ((*(buf+i+3))==0x07))
+ {
+ uPosFound = uReadPos+i;
+ break;
+ }
+
+ if (uPosFound!=0)
+ break;
+ }
+ TRYFREE(buf);
+ if (uPosFound == 0)
+ return 0;
+
+ /* Zip64 end of central directory locator */
+ if (ZSEEK64(*pzlib_filefunc_def,filestream, uPosFound,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ return 0;
+
+ /* the signature, already checked */
+ if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)
+ return 0;
+
+ /* number of the disk with the start of the zip64 end of central directory */
+ if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)
+ return 0;
+ if (uL != 0)
+ return 0;
+
+ /* relative offset of the zip64 end of central directory record */
+ if (unz64local_getLong64(pzlib_filefunc_def,filestream,&relativeOffset)!=UNZ_OK)
+ return 0;
+
+ /* total number of disks */
+ if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)
+ return 0;
+ if (uL != 1)
+ return 0;
+
+ /* Goto end of central directory record */
+ if (ZSEEK64(*pzlib_filefunc_def,filestream, relativeOffset,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ return 0;
+
+ /* the signature */
+ if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK)
+ return 0;
+
+ if (uL != 0x06064b50)
+ return 0;
+
+ return relativeOffset;
+}
+
+/*
+ Open a Zip file. path contain the full pathname (by example,
+ on a Windows NT computer "c:\\test\\zlib114.zip" or on an Unix computer
+ "zlib/zlib114.zip".
+ If the zipfile cannot be opened (file doesn't exist or in not valid), the
+ return value is NULL.
+ Else, the return value is a unzFile Handle, usable with other function
+ of this unzip package.
+*/
+local unzFile unzOpenInternal (const void *path,
+ zlib_filefunc64_32_def* pzlib_filefunc64_32_def,
+ int is64bitOpenFunction)
+{
+ unz64_s us;
+ unz64_s *s;
+ ZPOS64_T central_pos;
+ uLong uL;
+
+ uLong number_disk; /* number of the current dist, used for
+ spaning ZIP, unsupported, always 0*/
+ uLong number_disk_with_CD; /* number the the disk with central dir, used
+ for spaning ZIP, unsupported, always 0*/
+ ZPOS64_T number_entry_CD; /* total number of entries in
+ the central dir
+ (same than number_entry on nospan) */
+
+ int err=UNZ_OK;
+
+ if (unz_copyright[0]!=' ')
+ return NULL;
+
+ us.z_filefunc.zseek32_file = NULL;
+ us.z_filefunc.ztell32_file = NULL;
+ if (pzlib_filefunc64_32_def==NULL)
+ fill_fopen64_filefunc(&us.z_filefunc.zfile_func64);
+ else
+ us.z_filefunc = *pzlib_filefunc64_32_def;
+ us.is64bitOpenFunction = is64bitOpenFunction;
+
+
+
+ us.filestream = ZOPEN64(us.z_filefunc,
+ path,
+ ZLIB_FILEFUNC_MODE_READ |
+ ZLIB_FILEFUNC_MODE_EXISTING);
+ if (us.filestream==NULL)
+ return NULL;
+
+ central_pos = unz64local_SearchCentralDir64(&us.z_filefunc,us.filestream);
+ if (central_pos)
+ {
+ uLong uS;
+ ZPOS64_T uL64;
+
+ us.isZip64 = 1;
+
+ if (ZSEEK64(us.z_filefunc, us.filestream,
+ central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ err=UNZ_ERRNO;
+
+ /* the signature, already checked */
+ if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)
+ err=UNZ_ERRNO;
+
+ /* size of zip64 end of central directory record */
+ if (unz64local_getLong64(&us.z_filefunc, us.filestream,&uL64)!=UNZ_OK)
+ err=UNZ_ERRNO;
+
+ /* version made by */
+ if (unz64local_getShort(&us.z_filefunc, us.filestream,&uS)!=UNZ_OK)
+ err=UNZ_ERRNO;
+
+ /* version needed to extract */
+ if (unz64local_getShort(&us.z_filefunc, us.filestream,&uS)!=UNZ_OK)
+ err=UNZ_ERRNO;
+
+ /* number of this disk */
+ if (unz64local_getLong(&us.z_filefunc, us.filestream,&number_disk)!=UNZ_OK)
+ err=UNZ_ERRNO;
+
+ /* number of the disk with the start of the central directory */
+ if (unz64local_getLong(&us.z_filefunc, us.filestream,&number_disk_with_CD)!=UNZ_OK)
+ err=UNZ_ERRNO;
+
+ /* total number of entries in the central directory on this disk */
+ if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.gi.number_entry)!=UNZ_OK)
+ err=UNZ_ERRNO;
+
+ /* total number of entries in the central directory */
+ if (unz64local_getLong64(&us.z_filefunc, us.filestream,&number_entry_CD)!=UNZ_OK)
+ err=UNZ_ERRNO;
+
+ if ((number_entry_CD!=us.gi.number_entry) ||
+ (number_disk_with_CD!=0) ||
+ (number_disk!=0))
+ err=UNZ_BADZIPFILE;
+
+ /* size of the central directory */
+ if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.size_central_dir)!=UNZ_OK)
+ err=UNZ_ERRNO;
+
+ /* offset of start of central directory with respect to the
+ starting disk number */
+ if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.offset_central_dir)!=UNZ_OK)
+ err=UNZ_ERRNO;
+
+ us.gi.size_comment = 0;
+ }
+ else
+ {
+ central_pos = unz64local_SearchCentralDir(&us.z_filefunc,us.filestream);
+ if (central_pos==0)
+ err=UNZ_ERRNO;
+
+ us.isZip64 = 0;
+
+ if (ZSEEK64(us.z_filefunc, us.filestream,
+ central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ err=UNZ_ERRNO;
+
+ /* the signature, already checked */
+ if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)
+ err=UNZ_ERRNO;
+
+ /* number of this disk */
+ if (unz64local_getShort(&us.z_filefunc, us.filestream,&number_disk)!=UNZ_OK)
+ err=UNZ_ERRNO;
+
+ /* number of the disk with the start of the central directory */
+ if (unz64local_getShort(&us.z_filefunc, us.filestream,&number_disk_with_CD)!=UNZ_OK)
+ err=UNZ_ERRNO;
+
+ /* total number of entries in the central dir on this disk */
+ if (unz64local_getShort(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)
+ err=UNZ_ERRNO;
+ us.gi.number_entry = uL;
+
+ /* total number of entries in the central dir */
+ if (unz64local_getShort(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)
+ err=UNZ_ERRNO;
+ number_entry_CD = uL;
+
+ if ((number_entry_CD!=us.gi.number_entry) ||
+ (number_disk_with_CD!=0) ||
+ (number_disk!=0))
+ err=UNZ_BADZIPFILE;
+
+ /* size of the central directory */
+ if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)
+ err=UNZ_ERRNO;
+ us.size_central_dir = uL;
+
+ /* offset of start of central directory with respect to the
+ starting disk number */
+ if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK)
+ err=UNZ_ERRNO;
+ us.offset_central_dir = uL;
+
+ /* zipfile comment length */
+ if (unz64local_getShort(&us.z_filefunc, us.filestream,&us.gi.size_comment)!=UNZ_OK)
+ err=UNZ_ERRNO;
+ }
+
+ if ((central_pos<us.offset_central_dir+us.size_central_dir) &&
+ (err==UNZ_OK))
+ err=UNZ_BADZIPFILE;
+
+ if (err!=UNZ_OK)
+ {
+ ZCLOSE64(us.z_filefunc, us.filestream);
+ return NULL;
+ }
+
+ us.byte_before_the_zipfile = central_pos -
+ (us.offset_central_dir+us.size_central_dir);
+ us.central_pos = central_pos;
+ us.pfile_in_zip_read = NULL;
+ us.encrypted = 0;
+
+
+ s=(unz64_s*)ALLOC(sizeof(unz64_s));
+ if( s != NULL)
+ {
+ *s=us;
+ unzGoToFirstFile((unzFile)s);
+ }
+ return (unzFile)s;
+}
+
+
+extern unzFile ZEXPORT unzOpen2 (const char *path,
+ zlib_filefunc_def* pzlib_filefunc32_def)
+{
+ if (pzlib_filefunc32_def != NULL)
+ {
+ zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
+ fill_zlib_filefunc64_32_def_from_filefunc32(&zlib_filefunc64_32_def_fill,pzlib_filefunc32_def);
+ return unzOpenInternal(path, &zlib_filefunc64_32_def_fill, 0);
+ }
+ else
+ return unzOpenInternal(path, NULL, 0);
+}
+
+extern unzFile ZEXPORT unzOpen2_64 (const void *path,
+ zlib_filefunc64_def* pzlib_filefunc_def)
+{
+ if (pzlib_filefunc_def != NULL)
+ {
+ zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
+ zlib_filefunc64_32_def_fill.zfile_func64 = *pzlib_filefunc_def;
+ zlib_filefunc64_32_def_fill.ztell32_file = NULL;
+ zlib_filefunc64_32_def_fill.zseek32_file = NULL;
+ return unzOpenInternal(path, &zlib_filefunc64_32_def_fill, 1);
+ }
+ else
+ return unzOpenInternal(path, NULL, 1);
+}
+
+extern unzFile ZEXPORT unzOpen (const char *path)
+{
+ return unzOpenInternal(path, NULL, 0);
+}
+
+extern unzFile ZEXPORT unzOpen64 (const void *path)
+{
+ return unzOpenInternal(path, NULL, 1);
+}
+
+/*
+ Close a ZipFile opened with unzOpen.
+ If there is files inside the .Zip opened with unzOpenCurrentFile (see later),
+ these files MUST be closed with unzCloseCurrentFile before call unzClose.
+ return UNZ_OK if there is no problem. */
+extern int ZEXPORT unzClose (unzFile file)
+{
+ unz64_s* s;
+ if (file==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+
+ if (s->pfile_in_zip_read!=NULL)
+ unzCloseCurrentFile(file);
+
+ ZCLOSE64(s->z_filefunc, s->filestream);
+ TRYFREE(s);
+ return UNZ_OK;
+}
+
+
+/*
+ Write info about the ZipFile in the *pglobal_info structure.
+ No preparation of the structure is needed
+ return UNZ_OK if there is no problem. */
+extern int ZEXPORT unzGetGlobalInfo64 (unzFile file, unz_global_info64* pglobal_info)
+{
+ unz64_s* s;
+ if (file==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+ *pglobal_info=s->gi;
+ return UNZ_OK;
+}
+
+extern int ZEXPORT unzGetGlobalInfo (unzFile file, unz_global_info* pglobal_info32)
+{
+ unz64_s* s;
+ if (file==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+ /* to do : check if number_entry is not truncated */
+ pglobal_info32->number_entry = (uLong)s->gi.number_entry;
+ pglobal_info32->size_comment = s->gi.size_comment;
+ return UNZ_OK;
+}
+/*
+ Translate date/time from Dos format to tm_unz (readable more easilty)
+*/
+local void unz64local_DosDateToTmuDate (ZPOS64_T ulDosDate, tm_unz* ptm)
+{
+ ZPOS64_T uDate;
+ uDate = (ZPOS64_T)(ulDosDate>>16);
+ ptm->tm_mday = (uInt)(uDate&0x1f) ;
+ ptm->tm_mon = (uInt)((((uDate)&0x1E0)/0x20)-1) ;
+ ptm->tm_year = (uInt)(((uDate&0x0FE00)/0x0200)+1980) ;
+
+ ptm->tm_hour = (uInt) ((ulDosDate &0xF800)/0x800);
+ ptm->tm_min = (uInt) ((ulDosDate&0x7E0)/0x20) ;
+ ptm->tm_sec = (uInt) (2*(ulDosDate&0x1f)) ;
+}
+
+/*
+ Get Info about the current file in the zipfile, with internal only info
+*/
+local int unz64local_GetCurrentFileInfoInternal OF((unzFile file,
+ unz_file_info64 *pfile_info,
+ unz_file_info64_internal
+ *pfile_info_internal,
+ char *szFileName,
+ uLong fileNameBufferSize,
+ void *extraField,
+ uLong extraFieldBufferSize,
+ char *szComment,
+ uLong commentBufferSize));
+
+local int unz64local_GetCurrentFileInfoInternal (unzFile file,
+ unz_file_info64 *pfile_info,
+ unz_file_info64_internal
+ *pfile_info_internal,
+ char *szFileName,
+ uLong fileNameBufferSize,
+ void *extraField,
+ uLong extraFieldBufferSize,
+ char *szComment,
+ uLong commentBufferSize)
+{
+ unz64_s* s;
+ unz_file_info64 file_info;
+ unz_file_info64_internal file_info_internal;
+ int err=UNZ_OK;
+ uLong uMagic;
+ long lSeek=0;
+ uLong uL;
+
+ if (file==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+ if (ZSEEK64(s->z_filefunc, s->filestream,
+ s->pos_in_central_dir+s->byte_before_the_zipfile,
+ ZLIB_FILEFUNC_SEEK_SET)!=0)
+ err=UNZ_ERRNO;
+
+
+ /* we check the magic */
+ if (err==UNZ_OK)
+ {
+ if (unz64local_getLong(&s->z_filefunc, s->filestream,&uMagic) != UNZ_OK)
+ err=UNZ_ERRNO;
+ else if (uMagic!=0x02014b50)
+ err=UNZ_BADZIPFILE;
+ }
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.version) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.version_needed) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.flag) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.compression_method) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.dosDate) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ unz64local_DosDateToTmuDate(file_info.dosDate,&file_info.tmu_date);
+
+ if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.crc) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK)
+ err=UNZ_ERRNO;
+ file_info.compressed_size = uL;
+
+ if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK)
+ err=UNZ_ERRNO;
+ file_info.uncompressed_size = uL;
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_filename) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_file_extra) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_file_comment) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.disk_num_start) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.internal_fa) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.external_fa) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ // relative offset of local header
+ if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK)
+ err=UNZ_ERRNO;
+ file_info_internal.offset_curfile = uL;
+
+ lSeek+=file_info.size_filename;
+ if ((err==UNZ_OK) && (szFileName!=NULL))
+ {
+ uLong uSizeRead ;
+ if (file_info.size_filename<fileNameBufferSize)
+ {
+ *(szFileName+file_info.size_filename)='\0';
+ uSizeRead = file_info.size_filename;
+ }
+ else
+ uSizeRead = fileNameBufferSize;
+
+ if ((file_info.size_filename>0) && (fileNameBufferSize>0))
+ if (ZREAD64(s->z_filefunc, s->filestream,szFileName,uSizeRead)!=uSizeRead)
+ err=UNZ_ERRNO;
+ lSeek -= uSizeRead;
+ }
+
+ // Read extrafield
+ if ((err==UNZ_OK) && (extraField!=NULL))
+ {
+ ZPOS64_T uSizeRead ;
+ if (file_info.size_file_extra<extraFieldBufferSize)
+ uSizeRead = file_info.size_file_extra;
+ else
+ uSizeRead = extraFieldBufferSize;
+
+ if (lSeek!=0)
+ {
+ if (ZSEEK64(s->z_filefunc, s->filestream,lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)
+ lSeek=0;
+ else
+ err=UNZ_ERRNO;
+ }
+
+ if ((file_info.size_file_extra>0) && (extraFieldBufferSize>0))
+ if (ZREAD64(s->z_filefunc, s->filestream,extraField,(uLong)uSizeRead)!=uSizeRead)
+ err=UNZ_ERRNO;
+
+ lSeek += file_info.size_file_extra - (uLong)uSizeRead;
+ }
+ else
+ lSeek += file_info.size_file_extra;
+
+
+ if ((err==UNZ_OK) && (file_info.size_file_extra != 0))
+ {
+ uLong acc = 0;
+
+ // since lSeek now points to after the extra field we need to move back
+ lSeek -= file_info.size_file_extra;
+
+ if (lSeek!=0)
+ {
+ if (ZSEEK64(s->z_filefunc, s->filestream,lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)
+ lSeek=0;
+ else
+ err=UNZ_ERRNO;
+ }
+
+ while(acc < file_info.size_file_extra)
+ {
+ uLong headerId;
+ uLong dataSize;
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&headerId) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&dataSize) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ /* ZIP64 extra fields */
+ if (headerId == 0x0001)
+ {
+ uLong uL;
+
+ if(file_info.uncompressed_size == MAXU32)
+ {
+ if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info.uncompressed_size) != UNZ_OK)
+ err=UNZ_ERRNO;
+ }
+
+ if(file_info.compressed_size == MAXU32)
+ {
+ if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info.compressed_size) != UNZ_OK)
+ err=UNZ_ERRNO;
+ }
+
+ if(file_info_internal.offset_curfile == MAXU32)
+ {
+ /* Relative Header offset */
+ if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info_internal.offset_curfile) != UNZ_OK)
+ err=UNZ_ERRNO;
+ }
+
+ if(file_info.disk_num_start == MAXU32)
+ {
+ /* Disk Start Number */
+ if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK)
+ err=UNZ_ERRNO;
+ }
+
+ }
+ else
+ {
+ if (ZSEEK64(s->z_filefunc, s->filestream,dataSize,ZLIB_FILEFUNC_SEEK_CUR)!=0)
+ err=UNZ_ERRNO;
+ }
+
+ acc += 2 + 2 + dataSize;
+ }
+ }
+
+ if ((err==UNZ_OK) && (szComment!=NULL))
+ {
+ uLong uSizeRead ;
+ if (file_info.size_file_comment<commentBufferSize)
+ {
+ *(szComment+file_info.size_file_comment)='\0';
+ uSizeRead = file_info.size_file_comment;
+ }
+ else
+ uSizeRead = commentBufferSize;
+
+ if (lSeek!=0)
+ {
+ if (ZSEEK64(s->z_filefunc, s->filestream,lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0)
+ lSeek=0;
+ else
+ err=UNZ_ERRNO;
+ }
+
+ if ((file_info.size_file_comment>0) && (commentBufferSize>0))
+ if (ZREAD64(s->z_filefunc, s->filestream,szComment,uSizeRead)!=uSizeRead)
+ err=UNZ_ERRNO;
+ lSeek+=file_info.size_file_comment - uSizeRead;
+ }
+ else
+ lSeek+=file_info.size_file_comment;
+
+
+ if ((err==UNZ_OK) && (pfile_info!=NULL))
+ *pfile_info=file_info;
+
+ if ((err==UNZ_OK) && (pfile_info_internal!=NULL))
+ *pfile_info_internal=file_info_internal;
+
+ return err;
+}
+
+
+
+/*
+ Write info about the ZipFile in the *pglobal_info structure.
+ No preparation of the structure is needed
+ return UNZ_OK if there is no problem.
+*/
+extern int ZEXPORT unzGetCurrentFileInfo64 (unzFile file,
+ unz_file_info64 * pfile_info,
+ char * szFileName, uLong fileNameBufferSize,
+ void *extraField, uLong extraFieldBufferSize,
+ char* szComment, uLong commentBufferSize)
+{
+ return unz64local_GetCurrentFileInfoInternal(file,pfile_info,NULL,
+ szFileName,fileNameBufferSize,
+ extraField,extraFieldBufferSize,
+ szComment,commentBufferSize);
+}
+
+extern int ZEXPORT unzGetCurrentFileInfo (unzFile file,
+ unz_file_info * pfile_info,
+ char * szFileName, uLong fileNameBufferSize,
+ void *extraField, uLong extraFieldBufferSize,
+ char* szComment, uLong commentBufferSize)
+{
+ int err;
+ unz_file_info64 file_info64;
+ err = unz64local_GetCurrentFileInfoInternal(file,&file_info64,NULL,
+ szFileName,fileNameBufferSize,
+ extraField,extraFieldBufferSize,
+ szComment,commentBufferSize);
+ if ((err==UNZ_OK) && (pfile_info != NULL))
+ {
+ pfile_info->version = file_info64.version;
+ pfile_info->version_needed = file_info64.version_needed;
+ pfile_info->flag = file_info64.flag;
+ pfile_info->compression_method = file_info64.compression_method;
+ pfile_info->dosDate = file_info64.dosDate;
+ pfile_info->crc = file_info64.crc;
+
+ pfile_info->size_filename = file_info64.size_filename;
+ pfile_info->size_file_extra = file_info64.size_file_extra;
+ pfile_info->size_file_comment = file_info64.size_file_comment;
+
+ pfile_info->disk_num_start = file_info64.disk_num_start;
+ pfile_info->internal_fa = file_info64.internal_fa;
+ pfile_info->external_fa = file_info64.external_fa;
+
+ pfile_info->tmu_date = file_info64.tmu_date;
+
+
+ pfile_info->compressed_size = (uLong)file_info64.compressed_size;
+ pfile_info->uncompressed_size = (uLong)file_info64.uncompressed_size;
+
+ }
+ return err;
+}
+/*
+ Set the current file of the zipfile to the first file.
+ return UNZ_OK if there is no problem
+*/
+extern int ZEXPORT unzGoToFirstFile (unzFile file)
+{
+ int err=UNZ_OK;
+ unz64_s* s;
+ if (file==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+ s->pos_in_central_dir=s->offset_central_dir;
+ s->num_file=0;
+ err=unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info,
+ &s->cur_file_info_internal,
+ NULL,0,NULL,0,NULL,0);
+ s->current_file_ok = (err == UNZ_OK);
+ return err;
+}
+
+/*
+ Set the current file of the zipfile to the next file.
+ return UNZ_OK if there is no problem
+ return UNZ_END_OF_LIST_OF_FILE if the actual file was the latest.
+*/
+extern int ZEXPORT unzGoToNextFile (unzFile file)
+{
+ unz64_s* s;
+ int err;
+
+ if (file==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+ if (!s->current_file_ok)
+ return UNZ_END_OF_LIST_OF_FILE;
+ if (s->gi.number_entry != 0xffff) /* 2^16 files overflow hack */
+ if (s->num_file+1==s->gi.number_entry)
+ return UNZ_END_OF_LIST_OF_FILE;
+
+ s->pos_in_central_dir += SIZECENTRALDIRITEM + s->cur_file_info.size_filename +
+ s->cur_file_info.size_file_extra + s->cur_file_info.size_file_comment ;
+ s->num_file++;
+ err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info,
+ &s->cur_file_info_internal,
+ NULL,0,NULL,0,NULL,0);
+ s->current_file_ok = (err == UNZ_OK);
+ return err;
+}
+
+
+/*
+ Try locate the file szFileName in the zipfile.
+ For the iCaseSensitivity signification, see unzStringFileNameCompare
+
+ return value :
+ UNZ_OK if the file is found. It becomes the current file.
+ UNZ_END_OF_LIST_OF_FILE if the file is not found
+*/
+extern int ZEXPORT unzLocateFile (unzFile file, const char *szFileName, int iCaseSensitivity)
+{
+ unz64_s* s;
+ int err;
+
+ /* We remember the 'current' position in the file so that we can jump
+ * back there if we fail.
+ */
+ unz_file_info64 cur_file_infoSaved;
+ unz_file_info64_internal cur_file_info_internalSaved;
+ ZPOS64_T num_fileSaved;
+ ZPOS64_T pos_in_central_dirSaved;
+
+
+ if (file==NULL)
+ return UNZ_PARAMERROR;
+
+ if (strlen(szFileName)>=UNZ_MAXFILENAMEINZIP)
+ return UNZ_PARAMERROR;
+
+ s=(unz64_s*)file;
+ if (!s->current_file_ok)
+ return UNZ_END_OF_LIST_OF_FILE;
+
+ /* Save the current state */
+ num_fileSaved = s->num_file;
+ pos_in_central_dirSaved = s->pos_in_central_dir;
+ cur_file_infoSaved = s->cur_file_info;
+ cur_file_info_internalSaved = s->cur_file_info_internal;
+
+ err = unzGoToFirstFile(file);
+
+ while (err == UNZ_OK)
+ {
+ char szCurrentFileName[UNZ_MAXFILENAMEINZIP+1];
+ err = unzGetCurrentFileInfo64(file,NULL,
+ szCurrentFileName,sizeof(szCurrentFileName)-1,
+ NULL,0,NULL,0);
+ if (err == UNZ_OK)
+ {
+ if (unzStringFileNameCompare(szCurrentFileName,
+ szFileName,iCaseSensitivity)==0)
+ return UNZ_OK;
+ err = unzGoToNextFile(file);
+ }
+ }
+
+ /* We failed, so restore the state of the 'current file' to where we
+ * were.
+ */
+ s->num_file = num_fileSaved ;
+ s->pos_in_central_dir = pos_in_central_dirSaved ;
+ s->cur_file_info = cur_file_infoSaved;
+ s->cur_file_info_internal = cur_file_info_internalSaved;
+ return err;
+}
+
+
+/*
+///////////////////////////////////////////
+// Contributed by Ryan Haksi (mailto://cryogen@infoserve.net)
+// I need random access
+//
+// Further optimization could be realized by adding an ability
+// to cache the directory in memory. The goal being a single
+// comprehensive file read to put the file I need in a memory.
+*/
+
+/*
+typedef struct unz_file_pos_s
+{
+ ZPOS64_T pos_in_zip_directory; // offset in file
+ ZPOS64_T num_of_file; // # of file
+} unz_file_pos;
+*/
+
+extern int ZEXPORT unzGetFilePos64(unzFile file, unz64_file_pos* file_pos)
+{
+ unz64_s* s;
+
+ if (file==NULL || file_pos==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+ if (!s->current_file_ok)
+ return UNZ_END_OF_LIST_OF_FILE;
+
+ file_pos->pos_in_zip_directory = s->pos_in_central_dir;
+ file_pos->num_of_file = s->num_file;
+
+ return UNZ_OK;
+}
+
+extern int ZEXPORT unzGetFilePos(
+ unzFile file,
+ unz_file_pos* file_pos)
+{
+ unz64_file_pos file_pos64;
+ int err = unzGetFilePos64(file,&file_pos64);
+ if (err==UNZ_OK)
+ {
+ file_pos->pos_in_zip_directory = (uLong)file_pos64.pos_in_zip_directory;
+ file_pos->num_of_file = (uLong)file_pos64.num_of_file;
+ }
+ return err;
+}
+
+extern int ZEXPORT unzGoToFilePos64(unzFile file, const unz64_file_pos* file_pos)
+{
+ unz64_s* s;
+ int err;
+
+ if (file==NULL || file_pos==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+
+ /* jump to the right spot */
+ s->pos_in_central_dir = file_pos->pos_in_zip_directory;
+ s->num_file = file_pos->num_of_file;
+
+ /* set the current file */
+ err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info,
+ &s->cur_file_info_internal,
+ NULL,0,NULL,0,NULL,0);
+ /* return results */
+ s->current_file_ok = (err == UNZ_OK);
+ return err;
+}
+
+extern int ZEXPORT unzGoToFilePos(
+ unzFile file,
+ unz_file_pos* file_pos)
+{
+ unz64_file_pos file_pos64;
+ if (file_pos == NULL)
+ return UNZ_PARAMERROR;
+
+ file_pos64.pos_in_zip_directory = file_pos->pos_in_zip_directory;
+ file_pos64.num_of_file = file_pos->num_of_file;
+ return unzGoToFilePos64(file,&file_pos64);
+}
+
+/*
+// Unzip Helper Functions - should be here?
+///////////////////////////////////////////
+*/
+
+/*
+ Read the local header of the current zipfile
+ Check the coherency of the local header and info in the end of central
+ directory about this file
+ store in *piSizeVar the size of extra info in local header
+ (filename and size of extra field data)
+*/
+local int unz64local_CheckCurrentFileCoherencyHeader (unz64_s* s, uInt* piSizeVar,
+ ZPOS64_T * poffset_local_extrafield,
+ uInt * psize_local_extrafield)
+{
+ uLong uMagic,uData,uFlags;
+ uLong size_filename;
+ uLong size_extra_field;
+ int err=UNZ_OK;
+
+ *piSizeVar = 0;
+ *poffset_local_extrafield = 0;
+ *psize_local_extrafield = 0;
+
+ if (ZSEEK64(s->z_filefunc, s->filestream,s->cur_file_info_internal.offset_curfile +
+ s->byte_before_the_zipfile,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ return UNZ_ERRNO;
+
+
+ if (err==UNZ_OK)
+ {
+ if (unz64local_getLong(&s->z_filefunc, s->filestream,&uMagic) != UNZ_OK)
+ err=UNZ_ERRNO;
+ else if (uMagic!=0x04034b50)
+ err=UNZ_BADZIPFILE;
+ }
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&uData) != UNZ_OK)
+ err=UNZ_ERRNO;
+/*
+ else if ((err==UNZ_OK) && (uData!=s->cur_file_info.wVersion))
+ err=UNZ_BADZIPFILE;
+*/
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&uFlags) != UNZ_OK)
+ err=UNZ_ERRNO;
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&uData) != UNZ_OK)
+ err=UNZ_ERRNO;
+ else if ((err==UNZ_OK) && (uData!=s->cur_file_info.compression_method))
+ err=UNZ_BADZIPFILE;
+
+ if ((err==UNZ_OK) && (s->cur_file_info.compression_method!=0) &&
+/* #ifdef HAVE_BZIP2 */
+ (s->cur_file_info.compression_method!=Z_BZIP2ED) &&
+/* #endif */
+ (s->cur_file_info.compression_method!=Z_DEFLATED))
+ err=UNZ_BADZIPFILE;
+
+ if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* date/time */
+ err=UNZ_ERRNO;
+
+ if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* crc */
+ err=UNZ_ERRNO;
+ else if ((err==UNZ_OK) && (uData!=s->cur_file_info.crc) && ((uFlags & 8)==0))
+ err=UNZ_BADZIPFILE;
+
+ if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* size compr */
+ err=UNZ_ERRNO;
+ else if (uData != 0xFFFFFFFF && (err==UNZ_OK) && (uData!=s->cur_file_info.compressed_size) && ((uFlags & 8)==0))
+ err=UNZ_BADZIPFILE;
+
+ if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* size uncompr */
+ err=UNZ_ERRNO;
+ else if (uData != 0xFFFFFFFF && (err==UNZ_OK) && (uData!=s->cur_file_info.uncompressed_size) && ((uFlags & 8)==0))
+ err=UNZ_BADZIPFILE;
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&size_filename) != UNZ_OK)
+ err=UNZ_ERRNO;
+ else if ((err==UNZ_OK) && (size_filename!=s->cur_file_info.size_filename))
+ err=UNZ_BADZIPFILE;
+
+ *piSizeVar += (uInt)size_filename;
+
+ if (unz64local_getShort(&s->z_filefunc, s->filestream,&size_extra_field) != UNZ_OK)
+ err=UNZ_ERRNO;
+ *poffset_local_extrafield= s->cur_file_info_internal.offset_curfile +
+ SIZEZIPLOCALHEADER + size_filename;
+ *psize_local_extrafield = (uInt)size_extra_field;
+
+ *piSizeVar += (uInt)size_extra_field;
+
+ return err;
+}
+
+/*
+ Open for reading data the current file in the zipfile.
+ If there is no error and the file is opened, the return value is UNZ_OK.
+*/
+extern int ZEXPORT unzOpenCurrentFile3 (unzFile file, int* method,
+ int* level, int raw, const char* password)
+{
+ int err=UNZ_OK;
+ uInt iSizeVar;
+ unz64_s* s;
+ file_in_zip64_read_info_s* pfile_in_zip_read_info;
+ ZPOS64_T offset_local_extrafield; /* offset of the local extra field */
+ uInt size_local_extrafield; /* size of the local extra field */
+ if (password != NULL)
+ return UNZ_PARAMERROR;
+
+ if (file==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+ if (!s->current_file_ok)
+ return UNZ_PARAMERROR;
+
+ if (s->pfile_in_zip_read != NULL)
+ unzCloseCurrentFile(file);
+
+ if (unz64local_CheckCurrentFileCoherencyHeader(s,&iSizeVar, &offset_local_extrafield,&size_local_extrafield)!=UNZ_OK)
+ return UNZ_BADZIPFILE;
+
+ pfile_in_zip_read_info = (file_in_zip64_read_info_s*)ALLOC(sizeof(file_in_zip64_read_info_s));
+ if (pfile_in_zip_read_info==NULL)
+ return UNZ_INTERNALERROR;
+
+ pfile_in_zip_read_info->read_buffer=(char*)ALLOC(UNZ_BUFSIZE);
+ pfile_in_zip_read_info->offset_local_extrafield = offset_local_extrafield;
+ pfile_in_zip_read_info->size_local_extrafield = size_local_extrafield;
+ pfile_in_zip_read_info->pos_local_extrafield=0;
+ pfile_in_zip_read_info->raw=raw;
+
+ if (pfile_in_zip_read_info->read_buffer==NULL)
+ {
+ TRYFREE(pfile_in_zip_read_info);
+ return UNZ_INTERNALERROR;
+ }
+
+ pfile_in_zip_read_info->stream_initialised=0;
+
+ if (method!=NULL)
+ *method = (int)s->cur_file_info.compression_method;
+
+ if (level!=NULL)
+ {
+ *level = 6;
+ switch (s->cur_file_info.flag & 0x06)
+ {
+ case 6 : *level = 1; break;
+ case 4 : *level = 2; break;
+ case 2 : *level = 9; break;
+ }
+ }
+
+ if ((s->cur_file_info.compression_method!=0) &&
+/* #ifdef HAVE_BZIP2 */
+ (s->cur_file_info.compression_method!=Z_BZIP2ED) &&
+/* #endif */
+ (s->cur_file_info.compression_method!=Z_DEFLATED))
+
+ err=UNZ_BADZIPFILE;
+
+ pfile_in_zip_read_info->crc32_wait=s->cur_file_info.crc;
+ pfile_in_zip_read_info->crc32=0;
+ pfile_in_zip_read_info->total_out_64=0;
+ pfile_in_zip_read_info->compression_method = s->cur_file_info.compression_method;
+ pfile_in_zip_read_info->filestream=s->filestream;
+ pfile_in_zip_read_info->z_filefunc=s->z_filefunc;
+ pfile_in_zip_read_info->byte_before_the_zipfile=s->byte_before_the_zipfile;
+
+ pfile_in_zip_read_info->stream.total_out = 0;
+
+ if ((s->cur_file_info.compression_method==Z_BZIP2ED) && (!raw))
+ {
+#ifdef HAVE_BZIP2
+ pfile_in_zip_read_info->bstream.bzalloc = (void *(*) (void *, int, int))0;
+ pfile_in_zip_read_info->bstream.bzfree = (free_func)0;
+ pfile_in_zip_read_info->bstream.opaque = (voidpf)0;
+ pfile_in_zip_read_info->bstream.state = (voidpf)0;
+
+ pfile_in_zip_read_info->stream.zalloc = (alloc_func)0;
+ pfile_in_zip_read_info->stream.zfree = (free_func)0;
+ pfile_in_zip_read_info->stream.opaque = (voidpf)0;
+ pfile_in_zip_read_info->stream.next_in = (voidpf)0;
+ pfile_in_zip_read_info->stream.avail_in = 0;
+
+ err=BZ2_bzDecompressInit(&pfile_in_zip_read_info->bstream, 0, 0);
+ if (err == Z_OK)
+ pfile_in_zip_read_info->stream_initialised=Z_BZIP2ED;
+ else
+ {
+ TRYFREE(pfile_in_zip_read_info);
+ return err;
+ }
+#else
+ pfile_in_zip_read_info->raw=1;
+#endif
+ }
+ else if ((s->cur_file_info.compression_method==Z_DEFLATED) && (!raw))
+ {
+ pfile_in_zip_read_info->stream.zalloc = (alloc_func)0;
+ pfile_in_zip_read_info->stream.zfree = (free_func)0;
+ pfile_in_zip_read_info->stream.opaque = (voidpf)0;
+ pfile_in_zip_read_info->stream.next_in = 0;
+ pfile_in_zip_read_info->stream.avail_in = 0;
+
+ err=inflateInit2(&pfile_in_zip_read_info->stream, -MAX_WBITS);
+ if (err == Z_OK)
+ pfile_in_zip_read_info->stream_initialised=Z_DEFLATED;
+ else
+ {
+ TRYFREE(pfile_in_zip_read_info);
+ return err;
+ }
+ /* windowBits is passed < 0 to tell that there is no zlib header.
+ * Note that in this case inflate *requires* an extra "dummy" byte
+ * after the compressed stream in order to complete decompression and
+ * return Z_STREAM_END.
+ * In unzip, i don't wait absolutely Z_STREAM_END because I known the
+ * size of both compressed and uncompressed data
+ */
+ }
+ pfile_in_zip_read_info->rest_read_compressed =
+ s->cur_file_info.compressed_size ;
+ pfile_in_zip_read_info->rest_read_uncompressed =
+ s->cur_file_info.uncompressed_size ;
+
+
+ pfile_in_zip_read_info->pos_in_zipfile =
+ s->cur_file_info_internal.offset_curfile + SIZEZIPLOCALHEADER +
+ iSizeVar;
+
+ pfile_in_zip_read_info->stream.avail_in = (uInt)0;
+
+ s->pfile_in_zip_read = pfile_in_zip_read_info;
+ s->encrypted = 0;
+
+ return UNZ_OK;
+}
+
+extern int ZEXPORT unzOpenCurrentFile (unzFile file)
+{
+ return unzOpenCurrentFile3(file, NULL, NULL, 0, NULL);
+}
+
+extern int ZEXPORT unzOpenCurrentFilePassword (unzFile file, const char* password)
+{
+ return unzOpenCurrentFile3(file, NULL, NULL, 0, password);
+}
+
+extern int ZEXPORT unzOpenCurrentFile2 (unzFile file, int* method, int* level, int raw)
+{
+ return unzOpenCurrentFile3(file, method, level, raw, NULL);
+}
+
+/** Addition for GDAL : START */
+
+extern ZPOS64_T ZEXPORT unzGetCurrentFileZStreamPos64( unzFile file)
+{
+ unz64_s* s;
+ file_in_zip64_read_info_s* pfile_in_zip_read_info;
+ s=(unz64_s*)file;
+ if (file==NULL)
+ return 0; //UNZ_PARAMERROR;
+ pfile_in_zip_read_info=s->pfile_in_zip_read;
+ if (pfile_in_zip_read_info==NULL)
+ return 0; //UNZ_PARAMERROR;
+ return pfile_in_zip_read_info->pos_in_zipfile +
+ pfile_in_zip_read_info->byte_before_the_zipfile;
+}
+
+/** Addition for GDAL : END */
+
+/*
+ Read bytes from the current file.
+ buf contain buffer where data must be copied
+ len the size of buf.
+
+ return the number of byte copied if somes bytes are copied
+ return 0 if the end of file was reached
+ return <0 with error code if there is an error
+ (UNZ_ERRNO for IO error, or zLib error for uncompress error)
+*/
+extern int ZEXPORT unzReadCurrentFile (unzFile file, voidp buf, unsigned len)
+{
+ int err=UNZ_OK;
+ uInt iRead = 0;
+ unz64_s* s;
+ file_in_zip64_read_info_s* pfile_in_zip_read_info;
+ if (file==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+ pfile_in_zip_read_info=s->pfile_in_zip_read;
+
+ if (pfile_in_zip_read_info==NULL)
+ return UNZ_PARAMERROR;
+
+
+ if (pfile_in_zip_read_info->read_buffer == NULL)
+ return UNZ_END_OF_LIST_OF_FILE;
+ if (len==0)
+ return 0;
+
+ pfile_in_zip_read_info->stream.next_out = (Bytef*)buf;
+
+ pfile_in_zip_read_info->stream.avail_out = (uInt)len;
+
+ if ((len>pfile_in_zip_read_info->rest_read_uncompressed) &&
+ (!(pfile_in_zip_read_info->raw)))
+ pfile_in_zip_read_info->stream.avail_out =
+ (uInt)pfile_in_zip_read_info->rest_read_uncompressed;
+
+ if ((len>pfile_in_zip_read_info->rest_read_compressed+
+ pfile_in_zip_read_info->stream.avail_in) &&
+ (pfile_in_zip_read_info->raw))
+ pfile_in_zip_read_info->stream.avail_out =
+ (uInt)pfile_in_zip_read_info->rest_read_compressed+
+ pfile_in_zip_read_info->stream.avail_in;
+
+ while (pfile_in_zip_read_info->stream.avail_out>0)
+ {
+ if ((pfile_in_zip_read_info->stream.avail_in==0) &&
+ (pfile_in_zip_read_info->rest_read_compressed>0))
+ {
+ uInt uReadThis = UNZ_BUFSIZE;
+ if (pfile_in_zip_read_info->rest_read_compressed<uReadThis)
+ uReadThis = (uInt)pfile_in_zip_read_info->rest_read_compressed;
+ if (uReadThis == 0)
+ return UNZ_EOF;
+ if (ZSEEK64(pfile_in_zip_read_info->z_filefunc,
+ pfile_in_zip_read_info->filestream,
+ pfile_in_zip_read_info->pos_in_zipfile +
+ pfile_in_zip_read_info->byte_before_the_zipfile,
+ ZLIB_FILEFUNC_SEEK_SET)!=0)
+ return UNZ_ERRNO;
+ if (ZREAD64(pfile_in_zip_read_info->z_filefunc,
+ pfile_in_zip_read_info->filestream,
+ pfile_in_zip_read_info->read_buffer,
+ uReadThis)!=uReadThis)
+ return UNZ_ERRNO;
+
+ pfile_in_zip_read_info->pos_in_zipfile += uReadThis;
+
+ pfile_in_zip_read_info->rest_read_compressed-=uReadThis;
+
+ pfile_in_zip_read_info->stream.next_in =
+ (Bytef*)pfile_in_zip_read_info->read_buffer;
+ pfile_in_zip_read_info->stream.avail_in = (uInt)uReadThis;
+ }
+
+ if ((pfile_in_zip_read_info->compression_method==0) || (pfile_in_zip_read_info->raw))
+ {
+ uInt uDoCopy,i ;
+
+ if ((pfile_in_zip_read_info->stream.avail_in == 0) &&
+ (pfile_in_zip_read_info->rest_read_compressed == 0))
+ return (iRead==0) ? UNZ_EOF : iRead;
+
+ if (pfile_in_zip_read_info->stream.avail_out <
+ pfile_in_zip_read_info->stream.avail_in)
+ uDoCopy = pfile_in_zip_read_info->stream.avail_out ;
+ else
+ uDoCopy = pfile_in_zip_read_info->stream.avail_in ;
+
+ for (i=0;i<uDoCopy;i++)
+ *(pfile_in_zip_read_info->stream.next_out+i) =
+ *(pfile_in_zip_read_info->stream.next_in+i);
+
+ pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uDoCopy;
+
+ pfile_in_zip_read_info->crc32 = crc32(pfile_in_zip_read_info->crc32,
+ pfile_in_zip_read_info->stream.next_out,
+ uDoCopy);
+ pfile_in_zip_read_info->rest_read_uncompressed-=uDoCopy;
+ pfile_in_zip_read_info->stream.avail_in -= uDoCopy;
+ pfile_in_zip_read_info->stream.avail_out -= uDoCopy;
+ pfile_in_zip_read_info->stream.next_out += uDoCopy;
+ pfile_in_zip_read_info->stream.next_in += uDoCopy;
+ pfile_in_zip_read_info->stream.total_out += uDoCopy;
+ iRead += uDoCopy;
+ }
+ else if (pfile_in_zip_read_info->compression_method==Z_BZIP2ED)
+ {
+#ifdef HAVE_BZIP2
+ uLong uTotalOutBefore,uTotalOutAfter;
+ const Bytef *bufBefore;
+ uLong uOutThis;
+
+ pfile_in_zip_read_info->bstream.next_in = (char*)pfile_in_zip_read_info->stream.next_in;
+ pfile_in_zip_read_info->bstream.avail_in = pfile_in_zip_read_info->stream.avail_in;
+ pfile_in_zip_read_info->bstream.total_in_lo32 = pfile_in_zip_read_info->stream.total_in;
+ pfile_in_zip_read_info->bstream.total_in_hi32 = 0;
+ pfile_in_zip_read_info->bstream.next_out = (char*)pfile_in_zip_read_info->stream.next_out;
+ pfile_in_zip_read_info->bstream.avail_out = pfile_in_zip_read_info->stream.avail_out;
+ pfile_in_zip_read_info->bstream.total_out_lo32 = pfile_in_zip_read_info->stream.total_out;
+ pfile_in_zip_read_info->bstream.total_out_hi32 = 0;
+
+ uTotalOutBefore = pfile_in_zip_read_info->bstream.total_out_lo32;
+ bufBefore = (const Bytef *)pfile_in_zip_read_info->bstream.next_out;
+
+ err=BZ2_bzDecompress(&pfile_in_zip_read_info->bstream);
+
+ uTotalOutAfter = pfile_in_zip_read_info->bstream.total_out_lo32;
+ uOutThis = uTotalOutAfter-uTotalOutBefore;
+
+ pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uOutThis;
+
+ pfile_in_zip_read_info->crc32 = crc32(pfile_in_zip_read_info->crc32,bufBefore, (uInt)(uOutThis));
+ pfile_in_zip_read_info->rest_read_uncompressed -= uOutThis;
+ iRead += (uInt)(uTotalOutAfter - uTotalOutBefore);
+
+ pfile_in_zip_read_info->stream.next_in = (Bytef*)pfile_in_zip_read_info->bstream.next_in;
+ pfile_in_zip_read_info->stream.avail_in = pfile_in_zip_read_info->bstream.avail_in;
+ pfile_in_zip_read_info->stream.total_in = pfile_in_zip_read_info->bstream.total_in_lo32;
+ pfile_in_zip_read_info->stream.next_out = (Bytef*)pfile_in_zip_read_info->bstream.next_out;
+ pfile_in_zip_read_info->stream.avail_out = pfile_in_zip_read_info->bstream.avail_out;
+ pfile_in_zip_read_info->stream.total_out = pfile_in_zip_read_info->bstream.total_out_lo32;
+
+ if (err==BZ_STREAM_END)
+ return (iRead==0) ? UNZ_EOF : iRead;
+ if (err!=BZ_OK)
+ break;
+#endif
+ } // end Z_BZIP2ED
+ else
+ {
+ ZPOS64_T uTotalOutBefore,uTotalOutAfter;
+ const Bytef *bufBefore;
+ ZPOS64_T uOutThis;
+ int flush=Z_SYNC_FLUSH;
+
+ uTotalOutBefore = pfile_in_zip_read_info->stream.total_out;
+ bufBefore = pfile_in_zip_read_info->stream.next_out;
+
+ /*
+ if ((pfile_in_zip_read_info->rest_read_uncompressed ==
+ pfile_in_zip_read_info->stream.avail_out) &&
+ (pfile_in_zip_read_info->rest_read_compressed == 0))
+ flush = Z_FINISH;
+ */
+ err=inflate(&pfile_in_zip_read_info->stream,flush);
+
+ if ((err>=0) && (pfile_in_zip_read_info->stream.msg!=NULL))
+ err = Z_DATA_ERROR;
+
+ uTotalOutAfter = pfile_in_zip_read_info->stream.total_out;
+ uOutThis = uTotalOutAfter-uTotalOutBefore;
+
+ pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uOutThis;
+
+ pfile_in_zip_read_info->crc32 =
+ crc32(pfile_in_zip_read_info->crc32,bufBefore,
+ (uInt)(uOutThis));
+
+ pfile_in_zip_read_info->rest_read_uncompressed -=
+ uOutThis;
+
+ iRead += (uInt)(uTotalOutAfter - uTotalOutBefore);
+
+ if (err==Z_STREAM_END)
+ return (iRead==0) ? UNZ_EOF : iRead;
+ if (err!=Z_OK)
+ break;
+ }
+ }
+
+ if (err==Z_OK)
+ return iRead;
+ return err;
+}
+
+
+/*
+ Give the current position in uncompressed data
+*/
+extern z_off_t ZEXPORT unztell (unzFile file)
+{
+ unz64_s* s;
+ file_in_zip64_read_info_s* pfile_in_zip_read_info;
+ if (file==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+ pfile_in_zip_read_info=s->pfile_in_zip_read;
+
+ if (pfile_in_zip_read_info==NULL)
+ return UNZ_PARAMERROR;
+
+ return (z_off_t)pfile_in_zip_read_info->stream.total_out;
+}
+
+extern ZPOS64_T ZEXPORT unztell64 (unzFile file)
+{
+
+ unz64_s* s;
+ file_in_zip64_read_info_s* pfile_in_zip_read_info;
+ if (file==NULL)
+ return (ZPOS64_T)-1;
+ s=(unz64_s*)file;
+ pfile_in_zip_read_info=s->pfile_in_zip_read;
+
+ if (pfile_in_zip_read_info==NULL)
+ return (ZPOS64_T)-1;
+
+ return pfile_in_zip_read_info->total_out_64;
+}
+
+
+/*
+ return 1 if the end of file was reached, 0 elsewhere
+*/
+extern int ZEXPORT unzeof (unzFile file)
+{
+ unz64_s* s;
+ file_in_zip64_read_info_s* pfile_in_zip_read_info;
+ if (file==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+ pfile_in_zip_read_info=s->pfile_in_zip_read;
+
+ if (pfile_in_zip_read_info==NULL)
+ return UNZ_PARAMERROR;
+
+ if (pfile_in_zip_read_info->rest_read_uncompressed == 0)
+ return 1;
+ else
+ return 0;
+}
+
+
+
+/*
+Read extra field from the current file (opened by unzOpenCurrentFile)
+This is the local-header version of the extra field (sometimes, there is
+more info in the local-header version than in the central-header)
+
+ if buf==NULL, it return the size of the local extra field that can be read
+
+ if buf!=NULL, len is the size of the buffer, the extra header is copied in
+ buf.
+ the return value is the number of bytes copied in buf, or (if <0)
+ the error code
+*/
+extern int ZEXPORT unzGetLocalExtrafield (unzFile file, voidp buf, unsigned len)
+{
+ unz64_s* s;
+ file_in_zip64_read_info_s* pfile_in_zip_read_info;
+ uInt read_now;
+ ZPOS64_T size_to_read;
+
+ if (file==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+ pfile_in_zip_read_info=s->pfile_in_zip_read;
+
+ if (pfile_in_zip_read_info==NULL)
+ return UNZ_PARAMERROR;
+
+ size_to_read = (pfile_in_zip_read_info->size_local_extrafield -
+ pfile_in_zip_read_info->pos_local_extrafield);
+
+ if (buf==NULL)
+ return (int)size_to_read;
+
+ if (len>size_to_read)
+ read_now = (uInt)size_to_read;
+ else
+ read_now = (uInt)len ;
+
+ if (read_now==0)
+ return 0;
+
+ if (ZSEEK64(pfile_in_zip_read_info->z_filefunc,
+ pfile_in_zip_read_info->filestream,
+ pfile_in_zip_read_info->offset_local_extrafield +
+ pfile_in_zip_read_info->pos_local_extrafield,
+ ZLIB_FILEFUNC_SEEK_SET)!=0)
+ return UNZ_ERRNO;
+
+ if (ZREAD64(pfile_in_zip_read_info->z_filefunc,
+ pfile_in_zip_read_info->filestream,
+ buf,read_now)!=read_now)
+ return UNZ_ERRNO;
+
+ return (int)read_now;
+}
+
+/*
+ Close the file in zip opened with unzOpenCurrentFile
+ Return UNZ_CRCERROR if all the file was read but the CRC is not good
+*/
+extern int ZEXPORT unzCloseCurrentFile (unzFile file)
+{
+ int err=UNZ_OK;
+
+ unz64_s* s;
+ file_in_zip64_read_info_s* pfile_in_zip_read_info;
+ if (file==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+ pfile_in_zip_read_info=s->pfile_in_zip_read;
+
+ if (pfile_in_zip_read_info==NULL)
+ return UNZ_PARAMERROR;
+
+
+ if ((pfile_in_zip_read_info->rest_read_uncompressed == 0) &&
+ (!pfile_in_zip_read_info->raw))
+ {
+ if (pfile_in_zip_read_info->crc32 != pfile_in_zip_read_info->crc32_wait)
+ err=UNZ_CRCERROR;
+ }
+
+
+ TRYFREE(pfile_in_zip_read_info->read_buffer);
+ pfile_in_zip_read_info->read_buffer = NULL;
+ if (pfile_in_zip_read_info->stream_initialised == Z_DEFLATED)
+ inflateEnd(&pfile_in_zip_read_info->stream);
+#ifdef HAVE_BZIP2
+ else if (pfile_in_zip_read_info->stream_initialised == Z_BZIP2ED)
+ BZ2_bzDecompressEnd(&pfile_in_zip_read_info->bstream);
+#endif
+
+
+ pfile_in_zip_read_info->stream_initialised = 0;
+ TRYFREE(pfile_in_zip_read_info);
+
+ s->pfile_in_zip_read=NULL;
+
+ return err;
+}
+
+
+/*
+ Get the global comment string of the ZipFile, in the szComment buffer.
+ uSizeBuf is the size of the szComment buffer.
+ return the number of byte copied or an error code <0
+*/
+extern int ZEXPORT unzGetGlobalComment (unzFile file, char * szComment, uLong uSizeBuf)
+{
+ unz64_s* s;
+ uLong uReadThis ;
+ if (file==NULL)
+ return (int)UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+
+ uReadThis = uSizeBuf;
+ if (uReadThis>s->gi.size_comment)
+ uReadThis = s->gi.size_comment;
+
+ if (ZSEEK64(s->z_filefunc,s->filestream,s->central_pos+22,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ return UNZ_ERRNO;
+
+ if (uReadThis>0)
+ {
+ *szComment='\0';
+ if (ZREAD64(s->z_filefunc,s->filestream,szComment,uReadThis)!=uReadThis)
+ return UNZ_ERRNO;
+ }
+
+ if ((szComment != NULL) && (uSizeBuf > s->gi.size_comment))
+ *(szComment+s->gi.size_comment)='\0';
+ return (int)uReadThis;
+}
+
+/* Additions by RX '2004 */
+extern ZPOS64_T ZEXPORT unzGetOffset64(unzFile file)
+{
+ unz64_s* s;
+
+ if (file==NULL)
+ return 0; //UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+ if (!s->current_file_ok)
+ return 0;
+ if (s->gi.number_entry != 0 && s->gi.number_entry != 0xffff)
+ if (s->num_file==s->gi.number_entry)
+ return 0;
+ return s->pos_in_central_dir;
+}
+
+extern uLong ZEXPORT unzGetOffset (unzFile file)
+{
+ ZPOS64_T offset64;
+
+ if (file==NULL)
+ return 0; //UNZ_PARAMERROR;
+ offset64 = unzGetOffset64(file);
+ return (uLong)offset64;
+}
+
+extern int ZEXPORT unzSetOffset64(unzFile file, ZPOS64_T pos)
+{
+ unz64_s* s;
+ int err;
+
+ if (file==NULL)
+ return UNZ_PARAMERROR;
+ s=(unz64_s*)file;
+
+ s->pos_in_central_dir = pos;
+ s->num_file = s->gi.number_entry; /* hack */
+ err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info,
+ &s->cur_file_info_internal,
+ NULL,0,NULL,0,NULL,0);
+ s->current_file_ok = (err == UNZ_OK);
+ return err;
+}
+
+extern int ZEXPORT unzSetOffset (unzFile file, uLong pos)
+{
+ return unzSetOffset64(file,pos);
+}
diff --git a/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/unzip.h b/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/unzip.h
new file mode 100644
index 0000000..2104e39
--- /dev/null
+++ b/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/unzip.h
@@ -0,0 +1,437 @@
+/* unzip.h -- IO for uncompress .zip files using zlib
+ Version 1.1, February 14h, 2010
+ part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+ Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+ Modifications of Unzip for Zip64
+ Copyright (C) 2007-2008 Even Rouault
+
+ Modifications for Zip64 support on both zip and unzip
+ Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+ For more info read MiniZip_info.txt
+
+ ---------------------------------------------------------------------------------
+
+ Condition of use and distribution are the same than zlib :
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+
+ ---------------------------------------------------------------------------------
+
+ Changes
+
+ See header of unzip64.c
+
+*/
+
+#ifndef _unz64_H
+#define _unz64_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifndef _ZLIB_H
+#include "zlib.h"
+#endif
+
+#ifndef _ZLIBIOAPI_H
+#include "ioapi.h"
+#endif
+
+#ifdef HAVE_BZIP2
+#include "bzlib.h"
+#endif
+
+#define Z_BZIP2ED 12
+
+#if defined(STRICTUNZIP) || defined(STRICTZIPUNZIP)
+/* like the STRICT of WIN32, we define a pointer that cannot be converted
+ from (void*) without cast */
+typedef struct TagunzFile__ { int unused; } unzFile__;
+typedef unzFile__ *unzFile;
+#else
+typedef voidp unzFile;
+#endif
+
+
+#define UNZ_OK (0)
+#define UNZ_END_OF_LIST_OF_FILE (-100)
+#define UNZ_ERRNO (Z_ERRNO)
+#define UNZ_EOF (0)
+#define UNZ_PARAMERROR (-102)
+#define UNZ_BADZIPFILE (-103)
+#define UNZ_INTERNALERROR (-104)
+#define UNZ_CRCERROR (-105)
+
+/* tm_unz contain date/time info */
+typedef struct tm_unz_s
+{
+ uInt tm_sec; /* seconds after the minute - [0,59] */
+ uInt tm_min; /* minutes after the hour - [0,59] */
+ uInt tm_hour; /* hours since midnight - [0,23] */
+ uInt tm_mday; /* day of the month - [1,31] */
+ uInt tm_mon; /* months since January - [0,11] */
+ uInt tm_year; /* years - [1980..2044] */
+} tm_unz;
+
+/* unz_global_info structure contain global data about the ZIPfile
+ These data comes from the end of central dir */
+typedef struct unz_global_info64_s
+{
+ ZPOS64_T number_entry; /* total number of entries in
+ the central dir on this disk */
+ uLong size_comment; /* size of the global comment of the zipfile */
+} unz_global_info64;
+
+typedef struct unz_global_info_s
+{
+ uLong number_entry; /* total number of entries in
+ the central dir on this disk */
+ uLong size_comment; /* size of the global comment of the zipfile */
+} unz_global_info;
+
+/* unz_file_info contain information about a file in the zipfile */
+typedef struct unz_file_info64_s
+{
+ uLong version; /* version made by 2 bytes */
+ uLong version_needed; /* version needed to extract 2 bytes */
+ uLong flag; /* general purpose bit flag 2 bytes */
+ uLong compression_method; /* compression method 2 bytes */
+ uLong dosDate; /* last mod file date in Dos fmt 4 bytes */
+ uLong crc; /* crc-32 4 bytes */
+ ZPOS64_T compressed_size; /* compressed size 8 bytes */
+ ZPOS64_T uncompressed_size; /* uncompressed size 8 bytes */
+ uLong size_filename; /* filename length 2 bytes */
+ uLong size_file_extra; /* extra field length 2 bytes */
+ uLong size_file_comment; /* file comment length 2 bytes */
+
+ uLong disk_num_start; /* disk number start 2 bytes */
+ uLong internal_fa; /* internal file attributes 2 bytes */
+ uLong external_fa; /* external file attributes 4 bytes */
+
+ tm_unz tmu_date;
+} unz_file_info64;
+
+typedef struct unz_file_info_s
+{
+ uLong version; /* version made by 2 bytes */
+ uLong version_needed; /* version needed to extract 2 bytes */
+ uLong flag; /* general purpose bit flag 2 bytes */
+ uLong compression_method; /* compression method 2 bytes */
+ uLong dosDate; /* last mod file date in Dos fmt 4 bytes */
+ uLong crc; /* crc-32 4 bytes */
+ uLong compressed_size; /* compressed size 4 bytes */
+ uLong uncompressed_size; /* uncompressed size 4 bytes */
+ uLong size_filename; /* filename length 2 bytes */
+ uLong size_file_extra; /* extra field length 2 bytes */
+ uLong size_file_comment; /* file comment length 2 bytes */
+
+ uLong disk_num_start; /* disk number start 2 bytes */
+ uLong internal_fa; /* internal file attributes 2 bytes */
+ uLong external_fa; /* external file attributes 4 bytes */
+
+ tm_unz tmu_date;
+} unz_file_info;
+
+extern int ZEXPORT unzStringFileNameCompare OF ((const char* fileName1,
+ const char* fileName2,
+ int iCaseSensitivity));
+/*
+ Compare two filename (fileName1,fileName2).
+ If iCaseSenisivity = 1, comparision is case sensitivity (like strcmp)
+ If iCaseSenisivity = 2, comparision is not case sensitivity (like strcmpi
+ or strcasecmp)
+ If iCaseSenisivity = 0, case sensitivity is defaut of your operating system
+ (like 1 on Unix, 2 on Windows)
+*/
+
+
+extern unzFile ZEXPORT unzOpen OF((const char *path));
+extern unzFile ZEXPORT unzOpen64 OF((const void *path));
+/*
+ Open a Zip file. path contain the full pathname (by example,
+ on a Windows XP computer "c:\\zlib\\zlib113.zip" or on an Unix computer
+ "zlib/zlib113.zip".
+ If the zipfile cannot be opened (file don't exist or in not valid), the
+ return value is NULL.
+ Else, the return value is a unzFile Handle, usable with other function
+ of this unzip package.
+ the "64" function take a const void* pointer, because the path is just the
+ value passed to the open64_file_func callback.
+ Under Windows, if UNICODE is defined, using fill_fopen64_filefunc, the path
+ is a pointer to a wide unicode string (LPCTSTR is LPCWSTR), so const char*
+ does not describe the reality
+*/
+
+
+extern unzFile ZEXPORT unzOpen2 OF((const char *path,
+ zlib_filefunc_def* pzlib_filefunc_def));
+/*
+ Open a Zip file, like unzOpen, but provide a set of file low level API
+ for read/write the zip file (see ioapi.h)
+*/
+
+extern unzFile ZEXPORT unzOpen2_64 OF((const void *path,
+ zlib_filefunc64_def* pzlib_filefunc_def));
+/*
+ Open a Zip file, like unz64Open, but provide a set of file low level API
+ for read/write the zip file (see ioapi.h)
+*/
+
+extern int ZEXPORT unzClose OF((unzFile file));
+/*
+ Close a ZipFile opened with unzOpen.
+ If there is files inside the .Zip opened with unzOpenCurrentFile (see later),
+ these files MUST be closed with unzCloseCurrentFile before call unzClose.
+ return UNZ_OK if there is no problem. */
+
+extern int ZEXPORT unzGetGlobalInfo OF((unzFile file,
+ unz_global_info *pglobal_info));
+
+extern int ZEXPORT unzGetGlobalInfo64 OF((unzFile file,
+ unz_global_info64 *pglobal_info));
+/*
+ Write info about the ZipFile in the *pglobal_info structure.
+ No preparation of the structure is needed
+ return UNZ_OK if there is no problem. */
+
+
+extern int ZEXPORT unzGetGlobalComment OF((unzFile file,
+ char *szComment,
+ uLong uSizeBuf));
+/*
+ Get the global comment string of the ZipFile, in the szComment buffer.
+ uSizeBuf is the size of the szComment buffer.
+ return the number of byte copied or an error code <0
+*/
+
+
+/***************************************************************************/
+/* Unzip package allow you browse the directory of the zipfile */
+
+extern int ZEXPORT unzGoToFirstFile OF((unzFile file));
+/*
+ Set the current file of the zipfile to the first file.
+ return UNZ_OK if there is no problem
+*/
+
+extern int ZEXPORT unzGoToNextFile OF((unzFile file));
+/*
+ Set the current file of the zipfile to the next file.
+ return UNZ_OK if there is no problem
+ return UNZ_END_OF_LIST_OF_FILE if the actual file was the latest.
+*/
+
+extern int ZEXPORT unzLocateFile OF((unzFile file,
+ const char *szFileName,
+ int iCaseSensitivity));
+/*
+ Try locate the file szFileName in the zipfile.
+ For the iCaseSensitivity signification, see unzStringFileNameCompare
+
+ return value :
+ UNZ_OK if the file is found. It becomes the current file.
+ UNZ_END_OF_LIST_OF_FILE if the file is not found
+*/
+
+
+/* ****************************************** */
+/* Ryan supplied functions */
+/* unz_file_info contain information about a file in the zipfile */
+typedef struct unz_file_pos_s
+{
+ uLong pos_in_zip_directory; /* offset in zip file directory */
+ uLong num_of_file; /* # of file */
+} unz_file_pos;
+
+extern int ZEXPORT unzGetFilePos(
+ unzFile file,
+ unz_file_pos* file_pos);
+
+extern int ZEXPORT unzGoToFilePos(
+ unzFile file,
+ unz_file_pos* file_pos);
+
+typedef struct unz64_file_pos_s
+{
+ ZPOS64_T pos_in_zip_directory; /* offset in zip file directory */
+ ZPOS64_T num_of_file; /* # of file */
+} unz64_file_pos;
+
+extern int ZEXPORT unzGetFilePos64(
+ unzFile file,
+ unz64_file_pos* file_pos);
+
+extern int ZEXPORT unzGoToFilePos64(
+ unzFile file,
+ const unz64_file_pos* file_pos);
+
+/* ****************************************** */
+
+extern int ZEXPORT unzGetCurrentFileInfo64 OF((unzFile file,
+ unz_file_info64 *pfile_info,
+ char *szFileName,
+ uLong fileNameBufferSize,
+ void *extraField,
+ uLong extraFieldBufferSize,
+ char *szComment,
+ uLong commentBufferSize));
+
+extern int ZEXPORT unzGetCurrentFileInfo OF((unzFile file,
+ unz_file_info *pfile_info,
+ char *szFileName,
+ uLong fileNameBufferSize,
+ void *extraField,
+ uLong extraFieldBufferSize,
+ char *szComment,
+ uLong commentBufferSize));
+/*
+ Get Info about the current file
+ if pfile_info!=NULL, the *pfile_info structure will contain somes info about
+ the current file
+ if szFileName!=NULL, the filemane string will be copied in szFileName
+ (fileNameBufferSize is the size of the buffer)
+ if extraField!=NULL, the extra field information will be copied in extraField
+ (extraFieldBufferSize is the size of the buffer).
+ This is the Central-header version of the extra field
+ if szComment!=NULL, the comment string of the file will be copied in szComment
+ (commentBufferSize is the size of the buffer)
+*/
+
+
+/** Addition for GDAL : START */
+
+extern ZPOS64_T ZEXPORT unzGetCurrentFileZStreamPos64 OF((unzFile file));
+
+/** Addition for GDAL : END */
+
+
+/***************************************************************************/
+/* for reading the content of the current zipfile, you can open it, read data
+ from it, and close it (you can close it before reading all the file)
+ */
+
+extern int ZEXPORT unzOpenCurrentFile OF((unzFile file));
+/*
+ Open for reading data the current file in the zipfile.
+ If there is no error, the return value is UNZ_OK.
+*/
+
+extern int ZEXPORT unzOpenCurrentFilePassword OF((unzFile file,
+ const char* password));
+/*
+ Open for reading data the current file in the zipfile.
+ password is a crypting password
+ If there is no error, the return value is UNZ_OK.
+*/
+
+extern int ZEXPORT unzOpenCurrentFile2 OF((unzFile file,
+ int* method,
+ int* level,
+ int raw));
+/*
+ Same than unzOpenCurrentFile, but open for read raw the file (not uncompress)
+ if raw==1
+ *method will receive method of compression, *level will receive level of
+ compression
+ note : you can set level parameter as NULL (if you did not want known level,
+ but you CANNOT set method parameter as NULL
+*/
+
+extern int ZEXPORT unzOpenCurrentFile3 OF((unzFile file,
+ int* method,
+ int* level,
+ int raw,
+ const char* password));
+/*
+ Same than unzOpenCurrentFile, but open for read raw the file (not uncompress)
+ if raw==1
+ *method will receive method of compression, *level will receive level of
+ compression
+ note : you can set level parameter as NULL (if you did not want known level,
+ but you CANNOT set method parameter as NULL
+*/
+
+
+extern int ZEXPORT unzCloseCurrentFile OF((unzFile file));
+/*
+ Close the file in zip opened with unzOpenCurrentFile
+ Return UNZ_CRCERROR if all the file was read but the CRC is not good
+*/
+
+extern int ZEXPORT unzReadCurrentFile OF((unzFile file,
+ voidp buf,
+ unsigned len));
+/*
+ Read bytes from the current file (opened by unzOpenCurrentFile)
+ buf contain buffer where data must be copied
+ len the size of buf.
+
+ return the number of byte copied if somes bytes are copied
+ return 0 if the end of file was reached
+ return <0 with error code if there is an error
+ (UNZ_ERRNO for IO error, or zLib error for uncompress error)
+*/
+
+extern z_off_t ZEXPORT unztell OF((unzFile file));
+
+extern ZPOS64_T ZEXPORT unztell64 OF((unzFile file));
+/*
+ Give the current position in uncompressed data
+*/
+
+extern int ZEXPORT unzeof OF((unzFile file));
+/*
+ return 1 if the end of file was reached, 0 elsewhere
+*/
+
+extern int ZEXPORT unzGetLocalExtrafield OF((unzFile file,
+ voidp buf,
+ unsigned len));
+/*
+ Read extra field from the current file (opened by unzOpenCurrentFile)
+ This is the local-header version of the extra field (sometimes, there is
+ more info in the local-header version than in the central-header)
+
+ if buf==NULL, it return the size of the local extra field
+
+ if buf!=NULL, len is the size of the buffer, the extra header is copied in
+ buf.
+ the return value is the number of bytes copied in buf, or (if <0)
+ the error code
+*/
+
+/***************************************************************************/
+
+/* Get the current file offset */
+extern ZPOS64_T ZEXPORT unzGetOffset64 (unzFile file);
+extern uLong ZEXPORT unzGetOffset (unzFile file);
+
+/* Set the current file offset */
+extern int ZEXPORT unzSetOffset64 (unzFile file, ZPOS64_T pos);
+extern int ZEXPORT unzSetOffset (unzFile file, uLong pos);
+
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _unz64_H */
diff --git a/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/zip.c b/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/zip.c
new file mode 100644
index 0000000..9002d66
--- /dev/null
+++ b/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/zip.c
@@ -0,0 +1,1956 @@
+/* zip.c -- IO on .zip files using zlib
+ Version 1.1, February 14h, 2010
+ part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+ Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+ Modifications for Zip64 support
+ Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+ For more info read MiniZip_info.txt
+
+ Changes
+ Oct-2009 - Mathias Svensson - Remove old C style function prototypes
+ Oct-2009 - Mathias Svensson - Added Zip64 Support when creating new file archives
+ Oct-2009 - Mathias Svensson - Did some code cleanup and refactoring to get better overview of some functions.
+ Oct-2009 - Mathias Svensson - Added zipRemoveExtraInfoBlock to strip extra field data from its ZIP64 data
+ It is used when recreting zip archive with RAW when deleting items from a zip.
+ ZIP64 data is automatically added to items that needs it, and existing ZIP64 data need to be removed.
+ Oct-2009 - Mathias Svensson - Added support for BZIP2 as compression mode (bzip2 lib is required)
+ Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer
+
+*/
+
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include "zlib.h"
+#include "zip.h"
+
+#ifdef STDC
+# include <stddef.h>
+# include <string.h>
+# include <stdlib.h>
+#endif
+#ifdef NO_ERRNO_H
+ extern int errno;
+#else
+# include <errno.h>
+#endif
+
+
+#ifndef local
+# define local static
+#endif
+/* compile with -Dlocal if your debugger can't find static symbols */
+
+#ifndef VERSIONMADEBY
+# define VERSIONMADEBY (0x0) /* platform depedent */
+#endif
+
+#ifndef Z_BUFSIZE
+#define Z_BUFSIZE (64*1024) //(16384)
+#endif
+
+#ifndef Z_MAXFILENAMEINZIP
+#define Z_MAXFILENAMEINZIP (256)
+#endif
+
+#ifndef ALLOC
+# define ALLOC(size) (malloc(size))
+#endif
+#ifndef TRYFREE
+# define TRYFREE(p) {if (p) free(p);}
+#endif
+
+/*
+#define SIZECENTRALDIRITEM (0x2e)
+#define SIZEZIPLOCALHEADER (0x1e)
+*/
+
+/* I've found an old Unix (a SunOS 4.1.3_U1) without all SEEK_* defined.... */
+
+
+// NOT sure that this work on ALL platform
+#define MAKEULONG64(a, b) ((ZPOS64_T)(((unsigned long)(a)) | ((ZPOS64_T)((unsigned long)(b))) << 32))
+
+#ifndef SEEK_CUR
+#define SEEK_CUR 1
+#endif
+
+#ifndef SEEK_END
+#define SEEK_END 2
+#endif
+
+#ifndef SEEK_SET
+#define SEEK_SET 0
+#endif
+
+#ifndef DEF_MEM_LEVEL
+#if MAX_MEM_LEVEL >= 8
+# define DEF_MEM_LEVEL 8
+#else
+# define DEF_MEM_LEVEL MAX_MEM_LEVEL
+#endif
+#endif
+
+#define SIZEDATA_INDATABLOCK (4096-(4*4))
+
+#define LOCALHEADERMAGIC (0x04034b50)
+#define CENTRALHEADERMAGIC (0x02014b50)
+#define ENDHEADERMAGIC (0x06054b50)
+#define ZIP64ENDHEADERMAGIC (0x6064b50)
+#define ZIP64ENDLOCHEADERMAGIC (0x7064b50)
+
+#define FLAG_LOCALHEADER_OFFSET (0x06)
+#define CRC_LOCALHEADER_OFFSET (0x0e)
+
+#define SIZECENTRALHEADER (0x2e) /* 46 */
+
+typedef struct linkedlist_datablock_internal_s
+{
+ struct linkedlist_datablock_internal_s* next_datablock;
+ uLong avail_in_this_block;
+ uLong filled_in_this_block;
+ uLong unused; /* for future use and alignment */
+ unsigned char data[SIZEDATA_INDATABLOCK];
+} linkedlist_datablock_internal;
+
+typedef struct linkedlist_data_s
+{
+ linkedlist_datablock_internal* first_block;
+ linkedlist_datablock_internal* last_block;
+} linkedlist_data;
+
+
+typedef struct
+{
+ z_stream stream; /* zLib stream structure for inflate */
+#ifdef HAVE_BZIP2
+ bz_stream bstream; /* bzLib stream structure for bziped */
+#endif
+
+ int stream_initialised; /* 1 is stream is initialised */
+ uInt pos_in_buffered_data; /* last written byte in buffered_data */
+
+ ZPOS64_T pos_local_header; /* offset of the local header of the file
+ currenty writing */
+ char* central_header; /* central header data for the current file */
+ uLong size_centralExtra;
+ uLong size_centralheader; /* size of the central header for cur file */
+ uLong size_centralExtraFree; /* Extra bytes allocated to the centralheader but that are not used */
+ uLong flag; /* flag of the file currently writing */
+
+ int method; /* compression method of file currenty wr.*/
+ int raw; /* 1 for directly writing raw data */
+ Byte buffered_data[Z_BUFSIZE];/* buffer contain compressed data to be writ*/
+ uLong dosDate;
+ uLong crc32;
+ int encrypt;
+ int zip64; /* Add ZIP64 extened information in the extra field */
+ ZPOS64_T pos_zip64extrainfo;
+ ZPOS64_T totalCompressedData;
+ ZPOS64_T totalUncompressedData;
+} curfile64_info;
+
+typedef struct
+{
+ zlib_filefunc64_32_def z_filefunc;
+ voidpf filestream; /* io structore of the zipfile */
+ linkedlist_data central_dir;/* datablock with central dir in construction*/
+ int in_opened_file_inzip; /* 1 if a file in the zip is currently writ.*/
+ curfile64_info ci; /* info on the file curretly writing */
+
+ ZPOS64_T begin_pos; /* position of the beginning of the zipfile */
+ ZPOS64_T add_position_when_writing_offset;
+ ZPOS64_T number_entry;
+
+#ifndef NO_ADDFILEINEXISTINGZIP
+ char *globalcomment;
+#endif
+
+} zip64_internal;
+
+local linkedlist_datablock_internal* allocate_new_datablock()
+{
+ linkedlist_datablock_internal* ldi;
+ ldi = (linkedlist_datablock_internal*)
+ ALLOC(sizeof(linkedlist_datablock_internal));
+ if (ldi!=NULL)
+ {
+ ldi->next_datablock = NULL ;
+ ldi->filled_in_this_block = 0 ;
+ ldi->avail_in_this_block = SIZEDATA_INDATABLOCK ;
+ }
+ return ldi;
+}
+
+local void free_datablock(linkedlist_datablock_internal* ldi)
+{
+ while (ldi!=NULL)
+ {
+ linkedlist_datablock_internal* ldinext = ldi->next_datablock;
+ TRYFREE(ldi);
+ ldi = ldinext;
+ }
+}
+
+local void init_linkedlist(linkedlist_data* ll)
+{
+ ll->first_block = ll->last_block = NULL;
+}
+
+local void free_linkedlist(linkedlist_data* ll)
+{
+ free_datablock(ll->first_block);
+ ll->first_block = ll->last_block = NULL;
+}
+
+
+local int add_data_in_datablock(linkedlist_data* ll, const void* buf, uLong len)
+{
+ linkedlist_datablock_internal* ldi;
+ const unsigned char* from_copy;
+
+ if (ll==NULL)
+ return ZIP_INTERNALERROR;
+
+ if (ll->last_block == NULL)
+ {
+ ll->first_block = ll->last_block = allocate_new_datablock();
+ if (ll->first_block == NULL)
+ return ZIP_INTERNALERROR;
+ }
+
+ ldi = ll->last_block;
+ from_copy = (unsigned char*)buf;
+
+ while (len>0)
+ {
+ uInt copy_this;
+ uInt i;
+ unsigned char* to_copy;
+
+ if (ldi->avail_in_this_block==0)
+ {
+ ldi->next_datablock = allocate_new_datablock();
+ if (ldi->next_datablock == NULL)
+ return ZIP_INTERNALERROR;
+ ldi = ldi->next_datablock ;
+ ll->last_block = ldi;
+ }
+
+ if (ldi->avail_in_this_block < len)
+ copy_this = (uInt)ldi->avail_in_this_block;
+ else
+ copy_this = (uInt)len;
+
+ to_copy = &(ldi->data[ldi->filled_in_this_block]);
+
+ for (i=0;i<copy_this;i++)
+ *(to_copy+i)=*(from_copy+i);
+
+ ldi->filled_in_this_block += copy_this;
+ ldi->avail_in_this_block -= copy_this;
+ from_copy += copy_this ;
+ len -= copy_this;
+ }
+ return ZIP_OK;
+}
+
+
+
+/****************************************************************************/
+
+#ifndef NO_ADDFILEINEXISTINGZIP
+/* ===========================================================================
+ Inputs a long in LSB order to the given file
+ nbByte == 1, 2 ,4 or 8 (byte, short or long, ZPOS64_T)
+*/
+
+local int zip64local_putValue OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T x, int nbByte));
+local int zip64local_putValue (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T x, int nbByte)
+{
+ unsigned char buf[8];
+ int n;
+ for (n = 0; n < nbByte; n++)
+ {
+ buf[n] = (unsigned char)(x & 0xff);
+ x >>= 8;
+ }
+ if (x != 0)
+ { /* data overflow - hack for ZIP64 (X Roche) */
+ for (n = 0; n < nbByte; n++)
+ {
+ buf[n] = 0xff;
+ }
+ }
+
+ if (ZWRITE64(*pzlib_filefunc_def,filestream,buf,nbByte)!=(uLong)nbByte)
+ return ZIP_ERRNO;
+ else
+ return ZIP_OK;
+}
+
+local void zip64local_putValue_inmemory OF((void* dest, ZPOS64_T x, int nbByte));
+local void zip64local_putValue_inmemory (void* dest, ZPOS64_T x, int nbByte)
+{
+ unsigned char* buf=(unsigned char*)dest;
+ int n;
+ for (n = 0; n < nbByte; n++) {
+ buf[n] = (unsigned char)(x & 0xff);
+ x >>= 8;
+ }
+
+ if (x != 0)
+ { /* data overflow - hack for ZIP64 */
+ for (n = 0; n < nbByte; n++)
+ {
+ buf[n] = 0xff;
+ }
+ }
+}
+
+/****************************************************************************/
+
+
+local uLong zip64local_TmzDateToDosDate(const tm_zip* ptm)
+{
+ uLong year = (uLong)ptm->tm_year;
+ if (year>=1980)
+ year-=1980;
+ else if (year>=80)
+ year-=80;
+ return
+ (uLong) (((ptm->tm_mday) + (32 * (ptm->tm_mon+1)) + (512 * year)) << 16) |
+ ((ptm->tm_sec/2) + (32* ptm->tm_min) + (2048 * (uLong)ptm->tm_hour));
+}
+
+
+/****************************************************************************/
+
+local int zip64local_getByte OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, int *pi));
+
+local int zip64local_getByte(const zlib_filefunc64_32_def* pzlib_filefunc_def,voidpf filestream,int* pi)
+{
+ unsigned char c;
+ int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,&c,1);
+ if (err==1)
+ {
+ *pi = (int)c;
+ return ZIP_OK;
+ }
+ else
+ {
+ if (ZERROR64(*pzlib_filefunc_def,filestream))
+ return ZIP_ERRNO;
+ else
+ return ZIP_EOF;
+ }
+}
+
+
+/* ===========================================================================
+ Reads a long in LSB order from the given gz_stream. Sets
+*/
+local int zip64local_getShort OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong *pX));
+
+local int zip64local_getShort (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX)
+{
+ uLong x ;
+ int i = 0;
+ int err;
+
+ err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x = (uLong)i;
+
+ if (err==ZIP_OK)
+ err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x += ((uLong)i)<<8;
+
+ if (err==ZIP_OK)
+ *pX = x;
+ else
+ *pX = 0;
+ return err;
+}
+
+local int zip64local_getLong OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong *pX));
+
+local int zip64local_getLong (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, uLong* pX)
+{
+ uLong x ;
+ int i = 0;
+ int err;
+
+ err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x = (uLong)i;
+
+ if (err==ZIP_OK)
+ err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x += ((uLong)i)<<8;
+
+ if (err==ZIP_OK)
+ err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x += ((uLong)i)<<16;
+
+ if (err==ZIP_OK)
+ err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x += ((uLong)i)<<24;
+
+ if (err==ZIP_OK)
+ *pX = x;
+ else
+ *pX = 0;
+ return err;
+}
+
+local int zip64local_getLong64 OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T *pX));
+
+
+local int zip64local_getLong64 (const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, ZPOS64_T *pX)
+{
+ ZPOS64_T x;
+ int i = 0;
+ int err;
+
+ err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x = (ZPOS64_T)i;
+
+ if (err==ZIP_OK)
+ err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x += ((ZPOS64_T)i)<<8;
+
+ if (err==ZIP_OK)
+ err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x += ((ZPOS64_T)i)<<16;
+
+ if (err==ZIP_OK)
+ err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x += ((ZPOS64_T)i)<<24;
+
+ if (err==ZIP_OK)
+ err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x += ((ZPOS64_T)i)<<32;
+
+ if (err==ZIP_OK)
+ err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x += ((ZPOS64_T)i)<<40;
+
+ if (err==ZIP_OK)
+ err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x += ((ZPOS64_T)i)<<48;
+
+ if (err==ZIP_OK)
+ err = zip64local_getByte(pzlib_filefunc_def,filestream,&i);
+ x += ((ZPOS64_T)i)<<56;
+
+ if (err==ZIP_OK)
+ *pX = x;
+ else
+ *pX = 0;
+
+ return err;
+}
+
+#ifndef BUFREADCOMMENT
+#define BUFREADCOMMENT (0x400)
+#endif
+/*
+ Locate the Central directory of a zipfile (at the end, just before
+ the global comment)
+*/
+local ZPOS64_T zip64local_SearchCentralDir OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream));
+
+local ZPOS64_T zip64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream)
+{
+ unsigned char* buf;
+ ZPOS64_T uSizeFile;
+ ZPOS64_T uBackRead;
+ ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */
+ ZPOS64_T uPosFound=0;
+
+ if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)
+ return 0;
+
+
+ uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);
+
+ if (uMaxBack>uSizeFile)
+ uMaxBack = uSizeFile;
+
+ buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);
+ if (buf==NULL)
+ return 0;
+
+ uBackRead = 4;
+ while (uBackRead<uMaxBack)
+ {
+ uLong uReadSize;
+ ZPOS64_T uReadPos ;
+ int i;
+ if (uBackRead+BUFREADCOMMENT>uMaxBack)
+ uBackRead = uMaxBack;
+ else
+ uBackRead+=BUFREADCOMMENT;
+ uReadPos = uSizeFile-uBackRead ;
+
+ uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?
+ (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);
+ if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ break;
+
+ if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)
+ break;
+
+ for (i=(int)uReadSize-3; (i--)>0;)
+ if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) &&
+ ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06))
+ {
+ uPosFound = uReadPos+i;
+ break;
+ }
+
+ if (uPosFound!=0)
+ break;
+ }
+ TRYFREE(buf);
+ return uPosFound;
+}
+
+/*
+Locate the End of Zip64 Central directory locator and from there find the CD of a zipfile (at the end, just before
+the global comment)
+*/
+local ZPOS64_T zip64local_SearchCentralDir64 OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream));
+
+local ZPOS64_T zip64local_SearchCentralDir64(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream)
+{
+ unsigned char* buf;
+ ZPOS64_T uSizeFile;
+ ZPOS64_T uBackRead;
+ ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */
+ ZPOS64_T uPosFound=0;
+ uLong uL;
+ ZPOS64_T relativeOffset;
+
+ if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0)
+ return 0;
+
+ uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream);
+
+ if (uMaxBack>uSizeFile)
+ uMaxBack = uSizeFile;
+
+ buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4);
+ if (buf==NULL)
+ return 0;
+
+ uBackRead = 4;
+ while (uBackRead<uMaxBack)
+ {
+ uLong uReadSize;
+ ZPOS64_T uReadPos;
+ int i;
+ if (uBackRead+BUFREADCOMMENT>uMaxBack)
+ uBackRead = uMaxBack;
+ else
+ uBackRead+=BUFREADCOMMENT;
+ uReadPos = uSizeFile-uBackRead ;
+
+ uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ?
+ (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos);
+ if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ break;
+
+ if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize)
+ break;
+
+ for (i=(int)uReadSize-3; (i--)>0;)
+ {
+ // Signature "0x07064b50" Zip64 end of central directory locater
+ if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && ((*(buf+i+2))==0x06) && ((*(buf+i+3))==0x07))
+ {
+ uPosFound = uReadPos+i;
+ break;
+ }
+ }
+
+ if (uPosFound!=0)
+ break;
+ }
+
+ TRYFREE(buf);
+ if (uPosFound == 0)
+ return 0;
+
+ /* Zip64 end of central directory locator */
+ if (ZSEEK64(*pzlib_filefunc_def,filestream, uPosFound,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ return 0;
+
+ /* the signature, already checked */
+ if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
+ return 0;
+
+ /* number of the disk with the start of the zip64 end of central directory */
+ if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
+ return 0;
+ if (uL != 0)
+ return 0;
+
+ /* relative offset of the zip64 end of central directory record */
+ if (zip64local_getLong64(pzlib_filefunc_def,filestream,&relativeOffset)!=ZIP_OK)
+ return 0;
+
+ /* total number of disks */
+ if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
+ return 0;
+ if (uL != 1)
+ return 0;
+
+ /* Goto Zip64 end of central directory record */
+ if (ZSEEK64(*pzlib_filefunc_def,filestream, relativeOffset,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ return 0;
+
+ /* the signature */
+ if (zip64local_getLong(pzlib_filefunc_def,filestream,&uL)!=ZIP_OK)
+ return 0;
+
+ if (uL != 0x06064b50) // signature of 'Zip64 end of central directory'
+ return 0;
+
+ return relativeOffset;
+}
+
+int LoadCentralDirectoryRecord(zip64_internal* pziinit)
+{
+ int err=ZIP_OK;
+ ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/
+
+ ZPOS64_T size_central_dir; /* size of the central directory */
+ ZPOS64_T offset_central_dir; /* offset of start of central directory */
+ ZPOS64_T central_pos;
+ uLong uL;
+
+ uLong number_disk; /* number of the current dist, used for
+ spaning ZIP, unsupported, always 0*/
+ uLong number_disk_with_CD; /* number the the disk with central dir, used
+ for spaning ZIP, unsupported, always 0*/
+ ZPOS64_T number_entry;
+ ZPOS64_T number_entry_CD; /* total number of entries in
+ the central dir
+ (same than number_entry on nospan) */
+ uLong VersionMadeBy;
+ uLong VersionNeeded;
+ uLong size_comment;
+
+ int hasZIP64Record = 0;
+
+ // check first if we find a ZIP64 record
+ central_pos = zip64local_SearchCentralDir64(&pziinit->z_filefunc,pziinit->filestream);
+ if(central_pos > 0)
+ {
+ hasZIP64Record = 1;
+ }
+ else if(central_pos == 0)
+ {
+ central_pos = zip64local_SearchCentralDir(&pziinit->z_filefunc,pziinit->filestream);
+ }
+
+/* disable to allow appending to empty ZIP archive
+ if (central_pos==0)
+ err=ZIP_ERRNO;
+*/
+
+ if(hasZIP64Record)
+ {
+ ZPOS64_T sizeEndOfCentralDirectory;
+ if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, central_pos, ZLIB_FILEFUNC_SEEK_SET) != 0)
+ err=ZIP_ERRNO;
+
+ /* the signature, already checked */
+ if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&uL)!=ZIP_OK)
+ err=ZIP_ERRNO;
+
+ /* size of zip64 end of central directory record */
+ if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream, &sizeEndOfCentralDirectory)!=ZIP_OK)
+ err=ZIP_ERRNO;
+
+ /* version made by */
+ if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &VersionMadeBy)!=ZIP_OK)
+ err=ZIP_ERRNO;
+
+ /* version needed to extract */
+ if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &VersionNeeded)!=ZIP_OK)
+ err=ZIP_ERRNO;
+
+ /* number of this disk */
+ if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&number_disk)!=ZIP_OK)
+ err=ZIP_ERRNO;
+
+ /* number of the disk with the start of the central directory */
+ if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&number_disk_with_CD)!=ZIP_OK)
+ err=ZIP_ERRNO;
+
+ /* total number of entries in the central directory on this disk */
+ if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream, &number_entry)!=ZIP_OK)
+ err=ZIP_ERRNO;
+
+ /* total number of entries in the central directory */
+ if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&number_entry_CD)!=ZIP_OK)
+ err=ZIP_ERRNO;
+
+ if ((number_entry_CD!=number_entry) || (number_disk_with_CD!=0) || (number_disk!=0))
+ err=ZIP_BADZIPFILE;
+
+ /* size of the central directory */
+ if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&size_central_dir)!=ZIP_OK)
+ err=ZIP_ERRNO;
+
+ /* offset of start of central directory with respect to the
+ starting disk number */
+ if (zip64local_getLong64(&pziinit->z_filefunc, pziinit->filestream,&offset_central_dir)!=ZIP_OK)
+ err=ZIP_ERRNO;
+
+ // TODO..
+ // read the comment from the standard central header.
+ size_comment = 0;
+ }
+ else
+ {
+ // Read End of central Directory info
+ if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ err=ZIP_ERRNO;
+
+ /* the signature, already checked */
+ if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream,&uL)!=ZIP_OK)
+ err=ZIP_ERRNO;
+
+ /* number of this disk */
+ if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream,&number_disk)!=ZIP_OK)
+ err=ZIP_ERRNO;
+
+ /* number of the disk with the start of the central directory */
+ if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream,&number_disk_with_CD)!=ZIP_OK)
+ err=ZIP_ERRNO;
+
+ /* total number of entries in the central dir on this disk */
+ number_entry = 0;
+ if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
+ err=ZIP_ERRNO;
+ else
+ number_entry = uL;
+
+ /* total number of entries in the central dir */
+ number_entry_CD = 0;
+ if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
+ err=ZIP_ERRNO;
+ else
+ number_entry_CD = uL;
+
+ if ((number_entry_CD!=number_entry) || (number_disk_with_CD!=0) || (number_disk!=0))
+ err=ZIP_BADZIPFILE;
+
+ /* size of the central directory */
+ size_central_dir = 0;
+ if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
+ err=ZIP_ERRNO;
+ else
+ size_central_dir = uL;
+
+ /* offset of start of central directory with respect to the starting disk number */
+ offset_central_dir = 0;
+ if (zip64local_getLong(&pziinit->z_filefunc, pziinit->filestream, &uL)!=ZIP_OK)
+ err=ZIP_ERRNO;
+ else
+ offset_central_dir = uL;
+
+
+ /* zipfile global comment length */
+ if (zip64local_getShort(&pziinit->z_filefunc, pziinit->filestream, &size_comment)!=ZIP_OK)
+ err=ZIP_ERRNO;
+ }
+
+ if ((central_pos<offset_central_dir+size_central_dir) &&
+ (err==ZIP_OK))
+ err=ZIP_BADZIPFILE;
+
+ if (err!=ZIP_OK)
+ {
+ ZCLOSE64(pziinit->z_filefunc, pziinit->filestream);
+ return ZIP_ERRNO;
+ }
+
+ if (size_comment>0)
+ {
+ pziinit->globalcomment = (char*)ALLOC(size_comment+1);
+ if (pziinit->globalcomment)
+ {
+ size_comment = ZREAD64(pziinit->z_filefunc, pziinit->filestream, pziinit->globalcomment,size_comment);
+ pziinit->globalcomment[size_comment]=0;
+ }
+ }
+
+ byte_before_the_zipfile = central_pos - (offset_central_dir+size_central_dir);
+ pziinit->add_position_when_writing_offset = byte_before_the_zipfile;
+
+ {
+ ZPOS64_T size_central_dir_to_read = size_central_dir;
+ size_t buf_size = SIZEDATA_INDATABLOCK;
+ void* buf_read = (void*)ALLOC(buf_size);
+ if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, offset_central_dir + byte_before_the_zipfile, ZLIB_FILEFUNC_SEEK_SET) != 0)
+ err=ZIP_ERRNO;
+
+ while ((size_central_dir_to_read>0) && (err==ZIP_OK))
+ {
+ ZPOS64_T read_this = SIZEDATA_INDATABLOCK;
+ if (read_this > size_central_dir_to_read)
+ read_this = size_central_dir_to_read;
+
+ if (ZREAD64(pziinit->z_filefunc, pziinit->filestream,buf_read,(uLong)read_this) != read_this)
+ err=ZIP_ERRNO;
+
+ if (err==ZIP_OK)
+ err = add_data_in_datablock(&pziinit->central_dir,buf_read, (uLong)read_this);
+
+ size_central_dir_to_read-=read_this;
+ }
+ TRYFREE(buf_read);
+ }
+ pziinit->begin_pos = byte_before_the_zipfile;
+ pziinit->number_entry = number_entry_CD;
+
+ if (ZSEEK64(pziinit->z_filefunc, pziinit->filestream, offset_central_dir+byte_before_the_zipfile,ZLIB_FILEFUNC_SEEK_SET) != 0)
+ err=ZIP_ERRNO;
+
+ return err;
+}
+
+
+#endif /* !NO_ADDFILEINEXISTINGZIP*/
+
+
+/************************************************************/
+extern zipFile ZEXPORT zipOpen3 (const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_32_def* pzlib_filefunc64_32_def)
+{
+ zip64_internal ziinit;
+ zip64_internal* zi;
+ int err=ZIP_OK;
+
+ ziinit.z_filefunc.zseek32_file = NULL;
+ ziinit.z_filefunc.ztell32_file = NULL;
+ if (pzlib_filefunc64_32_def==NULL)
+ fill_fopen64_filefunc(&ziinit.z_filefunc.zfile_func64);
+ else
+ ziinit.z_filefunc = *pzlib_filefunc64_32_def;
+
+ ziinit.filestream = ZOPEN64(ziinit.z_filefunc,
+ pathname,
+ (append == APPEND_STATUS_CREATE) ?
+ (ZLIB_FILEFUNC_MODE_READ | ZLIB_FILEFUNC_MODE_WRITE | ZLIB_FILEFUNC_MODE_CREATE) :
+ (ZLIB_FILEFUNC_MODE_READ | ZLIB_FILEFUNC_MODE_WRITE | ZLIB_FILEFUNC_MODE_EXISTING));
+
+ if (ziinit.filestream == NULL)
+ return NULL;
+
+ if (append == APPEND_STATUS_CREATEAFTER)
+ ZSEEK64(ziinit.z_filefunc,ziinit.filestream,0,SEEK_END);
+
+ ziinit.begin_pos = ZTELL64(ziinit.z_filefunc,ziinit.filestream);
+ ziinit.in_opened_file_inzip = 0;
+ ziinit.ci.stream_initialised = 0;
+ ziinit.number_entry = 0;
+ ziinit.add_position_when_writing_offset = 0;
+ init_linkedlist(&(ziinit.central_dir));
+
+
+
+ zi = (zip64_internal*)ALLOC(sizeof(zip64_internal));
+ if (zi==NULL)
+ {
+ ZCLOSE64(ziinit.z_filefunc,ziinit.filestream);
+ return NULL;
+ }
+
+ /* now we add file in a zipfile */
+# ifndef NO_ADDFILEINEXISTINGZIP
+ ziinit.globalcomment = NULL;
+ if (append == APPEND_STATUS_ADDINZIP)
+ {
+ // Read and Cache Central Directory Records
+ err = LoadCentralDirectoryRecord(&ziinit);
+ }
+
+ if (globalcomment)
+ {
+ *globalcomment = ziinit.globalcomment;
+ }
+# endif /* !NO_ADDFILEINEXISTINGZIP*/
+
+ if (err != ZIP_OK)
+ {
+# ifndef NO_ADDFILEINEXISTINGZIP
+ TRYFREE(ziinit.globalcomment);
+# endif /* !NO_ADDFILEINEXISTINGZIP*/
+ TRYFREE(zi);
+ return NULL;
+ }
+ else
+ {
+ *zi = ziinit;
+ return (zipFile)zi;
+ }
+}
+
+extern zipFile ZEXPORT zipOpen2 (const char *pathname, int append, zipcharpc* globalcomment, zlib_filefunc_def* pzlib_filefunc32_def)
+{
+ if (pzlib_filefunc32_def != NULL)
+ {
+ zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
+ fill_zlib_filefunc64_32_def_from_filefunc32(&zlib_filefunc64_32_def_fill,pzlib_filefunc32_def);
+ return zipOpen3(pathname, append, globalcomment, &zlib_filefunc64_32_def_fill);
+ }
+ else
+ return zipOpen3(pathname, append, globalcomment, NULL);
+}
+
+extern zipFile ZEXPORT zipOpen2_64 (const void *pathname, int append, zipcharpc* globalcomment, zlib_filefunc64_def* pzlib_filefunc_def)
+{
+ if (pzlib_filefunc_def != NULL)
+ {
+ zlib_filefunc64_32_def zlib_filefunc64_32_def_fill;
+ zlib_filefunc64_32_def_fill.zfile_func64 = *pzlib_filefunc_def;
+ zlib_filefunc64_32_def_fill.ztell32_file = NULL;
+ zlib_filefunc64_32_def_fill.zseek32_file = NULL;
+ return zipOpen3(pathname, append, globalcomment, &zlib_filefunc64_32_def_fill);
+ }
+ else
+ return zipOpen3(pathname, append, globalcomment, NULL);
+}
+
+
+
+extern zipFile ZEXPORT zipOpen (const char* pathname, int append)
+{
+ return zipOpen3((const void*)pathname,append,NULL,NULL);
+}
+
+extern zipFile ZEXPORT zipOpen64 (const void* pathname, int append)
+{
+ return zipOpen3(pathname,append,NULL,NULL);
+}
+
+int Write_LocalFileHeader(zip64_internal* zi, const char* filename, uInt size_extrafield_local, const void* extrafield_local)
+{
+ /* write the local header */
+ int err;
+ uInt size_filename = (uInt)strlen(filename);
+ uInt size_extrafield = size_extrafield_local;
+
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)LOCALHEADERMAGIC, 4);
+
+ if (err==ZIP_OK)
+ {
+ if(zi->ci.zip64)
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);/* version needed to extract */
+ else
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)20,2);/* version needed to extract */
+ }
+
+ if (err==ZIP_OK)
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.flag,2);
+
+ if (err==ZIP_OK)
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.method,2);
+
+ if (err==ZIP_OK)
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->ci.dosDate,4);
+
+ // CRC / Compressed size / Uncompressed size will be filled in later and rewritten later
+ if (err==ZIP_OK)
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* crc 32, unknown */
+ if (err==ZIP_OK)
+ {
+ if(zi->ci.zip64)
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xFFFFFFFF,4); /* compressed size, unknown */
+ else
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* compressed size, unknown */
+ }
+ if (err==ZIP_OK)
+ {
+ if(zi->ci.zip64)
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xFFFFFFFF,4); /* uncompressed size, unknown */
+ else
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4); /* uncompressed size, unknown */
+ }
+
+ if (err==ZIP_OK)
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_filename,2);
+
+ if(zi->ci.zip64)
+ {
+ size_extrafield += 20;
+ }
+
+ if (err==ZIP_OK)
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_extrafield,2);
+
+ if ((err==ZIP_OK) && (size_filename > 0))
+ {
+ if (ZWRITE64(zi->z_filefunc,zi->filestream,filename,size_filename)!=size_filename)
+ err = ZIP_ERRNO;
+ }
+
+ if ((err==ZIP_OK) && (size_extrafield_local > 0))
+ {
+ if (ZWRITE64(zi->z_filefunc, zi->filestream, extrafield_local, size_extrafield_local) != size_extrafield_local)
+ err = ZIP_ERRNO;
+ }
+
+
+ if ((err==ZIP_OK) && (zi->ci.zip64))
+ {
+ // write the Zip64 extended info
+ short HeaderID = 1;
+ short DataSize = 16;
+ ZPOS64_T CompressedSize = 0;
+ ZPOS64_T UncompressedSize = 0;
+
+ // Remember position of Zip64 extended info for the local file header. (needed when we update size after done with file)
+ zi->ci.pos_zip64extrainfo = ZTELL64(zi->z_filefunc,zi->filestream);
+
+ err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (short)HeaderID,2);
+ err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (short)DataSize,2);
+
+ err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)UncompressedSize,8);
+ err = zip64local_putValue(&zi->z_filefunc, zi->filestream, (ZPOS64_T)CompressedSize,8);
+ }
+
+ return err;
+}
+
+/*
+ NOTE.
+ When writing RAW the ZIP64 extended information in extrafield_local and extrafield_global needs to be stripped
+ before calling this function it can be done with zipRemoveExtraInfoBlock
+
+ It is not done here because then we need to realloc a new buffer since parameters are 'const' and I want to minimize
+ unnecessary allocations.
+ */
+extern int ZEXPORT zipOpenNewFileInZip4_64 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
+ const void* extrafield_local, uInt size_extrafield_local,
+ const void* extrafield_global, uInt size_extrafield_global,
+ const char* comment, int method, int level, int raw,
+ int windowBits,int memLevel, int strategy,
+ const char* password, uLong crcForCrypting,
+ uLong versionMadeBy, uLong flagBase, int zip64)
+{
+ zip64_internal* zi;
+ uInt size_filename;
+ uInt size_comment;
+ uInt i;
+ int err = ZIP_OK;
+
+ if (file == NULL)
+ return ZIP_PARAMERROR;
+
+#ifdef HAVE_BZIP2
+ if ((method!=0) && (method!=Z_DEFLATED) && (method!=Z_BZIP2ED))
+ return ZIP_PARAMERROR;
+#else
+ if ((method!=0) && (method!=Z_DEFLATED))
+ return ZIP_PARAMERROR;
+#endif
+
+ zi = (zip64_internal*)file;
+
+ if (zi->in_opened_file_inzip == 1)
+ {
+ err = zipCloseFileInZip (file);
+ if (err != ZIP_OK)
+ return err;
+ }
+
+ if (filename==NULL)
+ filename="-";
+
+ if (comment==NULL)
+ size_comment = 0;
+ else
+ size_comment = (uInt)strlen(comment);
+
+ size_filename = (uInt)strlen(filename);
+
+ if (zipfi == NULL)
+ zi->ci.dosDate = 0;
+ else
+ {
+ if (zipfi->dosDate != 0)
+ zi->ci.dosDate = zipfi->dosDate;
+ else
+ zi->ci.dosDate = zip64local_TmzDateToDosDate(&zipfi->tmz_date);
+ }
+
+ zi->ci.flag = flagBase;
+ if ((level==8) || (level==9))
+ zi->ci.flag |= 2;
+ if (level==2)
+ zi->ci.flag |= 4;
+ if (level==1)
+ zi->ci.flag |= 6;
+ if (password != NULL)
+ zi->ci.flag |= 1;
+
+ zi->ci.crc32 = 0;
+ zi->ci.method = method;
+ zi->ci.encrypt = 0;
+ zi->ci.stream_initialised = 0;
+ zi->ci.pos_in_buffered_data = 0;
+ zi->ci.raw = raw;
+ zi->ci.pos_local_header = ZTELL64(zi->z_filefunc,zi->filestream);
+
+ zi->ci.size_centralheader = SIZECENTRALHEADER + size_filename + size_extrafield_global + size_comment;
+ zi->ci.size_centralExtraFree = 32; // Extra space we have reserved in case we need to add ZIP64 extra info data
+
+ zi->ci.central_header = (char*)ALLOC((uInt)zi->ci.size_centralheader + zi->ci.size_centralExtraFree);
+
+ zi->ci.size_centralExtra = size_extrafield_global;
+ zip64local_putValue_inmemory(zi->ci.central_header,(uLong)CENTRALHEADERMAGIC,4);
+ /* version info */
+ zip64local_putValue_inmemory(zi->ci.central_header+4,(uLong)versionMadeBy,2);
+ zip64local_putValue_inmemory(zi->ci.central_header+6,(uLong)20,2);
+ zip64local_putValue_inmemory(zi->ci.central_header+8,(uLong)zi->ci.flag,2);
+ zip64local_putValue_inmemory(zi->ci.central_header+10,(uLong)zi->ci.method,2);
+ zip64local_putValue_inmemory(zi->ci.central_header+12,(uLong)zi->ci.dosDate,4);
+ zip64local_putValue_inmemory(zi->ci.central_header+16,(uLong)0,4); /*crc*/
+ zip64local_putValue_inmemory(zi->ci.central_header+20,(uLong)0,4); /*compr size*/
+ zip64local_putValue_inmemory(zi->ci.central_header+24,(uLong)0,4); /*uncompr size*/
+ zip64local_putValue_inmemory(zi->ci.central_header+28,(uLong)size_filename,2);
+ zip64local_putValue_inmemory(zi->ci.central_header+30,(uLong)size_extrafield_global,2);
+ zip64local_putValue_inmemory(zi->ci.central_header+32,(uLong)size_comment,2);
+ zip64local_putValue_inmemory(zi->ci.central_header+34,(uLong)0,2); /*disk nm start*/
+
+ if (zipfi==NULL)
+ zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)0,2);
+ else
+ zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)zipfi->internal_fa,2);
+
+ if (zipfi==NULL)
+ zip64local_putValue_inmemory(zi->ci.central_header+38,(uLong)0,4);
+ else
+ zip64local_putValue_inmemory(zi->ci.central_header+38,(uLong)zipfi->external_fa,4);
+
+ if(zi->ci.pos_local_header >= 0xffffffff)
+ zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)0xffffffff,4);
+ else
+ zip64local_putValue_inmemory(zi->ci.central_header+42,(uLong)zi->ci.pos_local_header - zi->add_position_when_writing_offset,4);
+
+ for (i=0;i<size_filename;i++)
+ *(zi->ci.central_header+SIZECENTRALHEADER+i) = *(filename+i);
+
+ for (i=0;i<size_extrafield_global;i++)
+ *(zi->ci.central_header+SIZECENTRALHEADER+size_filename+i) =
+ *(((const char*)extrafield_global)+i);
+
+ for (i=0;i<size_comment;i++)
+ *(zi->ci.central_header+SIZECENTRALHEADER+size_filename+
+ size_extrafield_global+i) = *(comment+i);
+ if (zi->ci.central_header == NULL)
+ return ZIP_INTERNALERROR;
+
+ zi->ci.zip64 = zip64;
+ zi->ci.totalCompressedData = 0;
+ zi->ci.totalUncompressedData = 0;
+ zi->ci.pos_zip64extrainfo = 0;
+
+ err = Write_LocalFileHeader(zi, filename, size_extrafield_local, extrafield_local);
+
+#ifdef HAVE_BZIP2
+ zi->ci.bstream.avail_in = (uInt)0;
+ zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;
+ zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;
+ zi->ci.bstream.total_in_hi32 = 0;
+ zi->ci.bstream.total_in_lo32 = 0;
+ zi->ci.bstream.total_out_hi32 = 0;
+ zi->ci.bstream.total_out_lo32 = 0;
+#endif
+
+ zi->ci.stream.avail_in = (uInt)0;
+ zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;
+ zi->ci.stream.next_out = zi->ci.buffered_data;
+ zi->ci.stream.total_in = 0;
+ zi->ci.stream.total_out = 0;
+ zi->ci.stream.data_type = Z_BINARY;
+
+#ifdef HAVE_BZIP2
+ if ((err==ZIP_OK) && (zi->ci.method == Z_DEFLATED || zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
+#else
+ if ((err==ZIP_OK) && (zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
+#endif
+ {
+ if(zi->ci.method == Z_DEFLATED)
+ {
+ zi->ci.stream.zalloc = (alloc_func)0;
+ zi->ci.stream.zfree = (free_func)0;
+ zi->ci.stream.opaque = (voidpf)0;
+
+ if (windowBits>0)
+ windowBits = -windowBits;
+
+ err = deflateInit2(&zi->ci.stream, level, Z_DEFLATED, windowBits, memLevel, strategy);
+
+ if (err==Z_OK)
+ zi->ci.stream_initialised = Z_DEFLATED;
+ }
+ else if(zi->ci.method == Z_BZIP2ED)
+ {
+#ifdef HAVE_BZIP2
+ // Init BZip stuff here
+ zi->ci.bstream.bzalloc = 0;
+ zi->ci.bstream.bzfree = 0;
+ zi->ci.bstream.opaque = (voidpf)0;
+
+ err = BZ2_bzCompressInit(&zi->ci.bstream, level, 0,35);
+ if(err == BZ_OK)
+ zi->ci.stream_initialised = Z_BZIP2ED;
+#endif
+ }
+
+ }
+
+ if (err==Z_OK)
+ zi->in_opened_file_inzip = 1;
+ return err;
+}
+
+extern int ZEXPORT zipOpenNewFileInZip4 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
+ const void* extrafield_local, uInt size_extrafield_local,
+ const void* extrafield_global, uInt size_extrafield_global,
+ const char* comment, int method, int level, int raw,
+ int windowBits,int memLevel, int strategy,
+ const char* password, uLong crcForCrypting,
+ uLong versionMadeBy, uLong flagBase)
+{
+ return zipOpenNewFileInZip4_64 (file, filename, zipfi,
+ extrafield_local, size_extrafield_local,
+ extrafield_global, size_extrafield_global,
+ comment, method, level, raw,
+ windowBits, memLevel, strategy,
+ password, crcForCrypting, versionMadeBy, flagBase, 0);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip3 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
+ const void* extrafield_local, uInt size_extrafield_local,
+ const void* extrafield_global, uInt size_extrafield_global,
+ const char* comment, int method, int level, int raw,
+ int windowBits,int memLevel, int strategy,
+ const char* password, uLong crcForCrypting)
+{
+ return zipOpenNewFileInZip4_64 (file, filename, zipfi,
+ extrafield_local, size_extrafield_local,
+ extrafield_global, size_extrafield_global,
+ comment, method, level, raw,
+ windowBits, memLevel, strategy,
+ password, crcForCrypting, VERSIONMADEBY, 0, 0);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip3_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+ const void* extrafield_local, uInt size_extrafield_local,
+ const void* extrafield_global, uInt size_extrafield_global,
+ const char* comment, int method, int level, int raw,
+ int windowBits,int memLevel, int strategy,
+ const char* password, uLong crcForCrypting, int zip64)
+{
+ return zipOpenNewFileInZip4_64 (file, filename, zipfi,
+ extrafield_local, size_extrafield_local,
+ extrafield_global, size_extrafield_global,
+ comment, method, level, raw,
+ windowBits, memLevel, strategy,
+ password, crcForCrypting, VERSIONMADEBY, 0, zip64);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip2(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+ const void* extrafield_local, uInt size_extrafield_local,
+ const void* extrafield_global, uInt size_extrafield_global,
+ const char* comment, int method, int level, int raw)
+{
+ return zipOpenNewFileInZip4_64 (file, filename, zipfi,
+ extrafield_local, size_extrafield_local,
+ extrafield_global, size_extrafield_global,
+ comment, method, level, raw,
+ -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+ NULL, 0, VERSIONMADEBY, 0, 0);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip2_64(zipFile file, const char* filename, const zip_fileinfo* zipfi,
+ const void* extrafield_local, uInt size_extrafield_local,
+ const void* extrafield_global, uInt size_extrafield_global,
+ const char* comment, int method, int level, int raw, int zip64)
+{
+ return zipOpenNewFileInZip4_64 (file, filename, zipfi,
+ extrafield_local, size_extrafield_local,
+ extrafield_global, size_extrafield_global,
+ comment, method, level, raw,
+ -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+ NULL, 0, VERSIONMADEBY, 0, zip64);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip64 (zipFile file, const char* filename, const zip_fileinfo* zipfi,
+ const void* extrafield_local, uInt size_extrafield_local,
+ const void*extrafield_global, uInt size_extrafield_global,
+ const char* comment, int method, int level, int zip64)
+{
+ return zipOpenNewFileInZip4_64 (file, filename, zipfi,
+ extrafield_local, size_extrafield_local,
+ extrafield_global, size_extrafield_global,
+ comment, method, level, 0,
+ -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+ NULL, 0, VERSIONMADEBY, 0, zip64);
+}
+
+extern int ZEXPORT zipOpenNewFileInZip (zipFile file, const char* filename, const zip_fileinfo* zipfi,
+ const void* extrafield_local, uInt size_extrafield_local,
+ const void*extrafield_global, uInt size_extrafield_global,
+ const char* comment, int method, int level)
+{
+ return zipOpenNewFileInZip4_64 (file, filename, zipfi,
+ extrafield_local, size_extrafield_local,
+ extrafield_global, size_extrafield_global,
+ comment, method, level, 0,
+ -MAX_WBITS, DEF_MEM_LEVEL, Z_DEFAULT_STRATEGY,
+ NULL, 0, VERSIONMADEBY, 0, 0);
+}
+
+local int zip64FlushWriteBuffer(zip64_internal* zi)
+{
+ int err=ZIP_OK;
+
+ if (ZWRITE64(zi->z_filefunc,zi->filestream,zi->ci.buffered_data,zi->ci.pos_in_buffered_data) != zi->ci.pos_in_buffered_data)
+ err = ZIP_ERRNO;
+
+ zi->ci.totalCompressedData += zi->ci.pos_in_buffered_data;
+
+#ifdef HAVE_BZIP2
+ if(zi->ci.method == Z_BZIP2ED)
+ {
+ zi->ci.totalUncompressedData += zi->ci.bstream.total_in_lo32;
+ zi->ci.bstream.total_in_lo32 = 0;
+ zi->ci.bstream.total_in_hi32 = 0;
+ }
+ else
+#endif
+ {
+ zi->ci.totalUncompressedData += zi->ci.stream.total_in;
+ zi->ci.stream.total_in = 0;
+ }
+
+
+ zi->ci.pos_in_buffered_data = 0;
+
+ return err;
+}
+
+extern int ZEXPORT zipWriteInFileInZip (zipFile file,const void* buf,unsigned int len)
+{
+ zip64_internal* zi;
+ int err=ZIP_OK;
+
+ if (file == NULL)
+ return ZIP_PARAMERROR;
+ zi = (zip64_internal*)file;
+
+ if (zi->in_opened_file_inzip == 0)
+ return ZIP_PARAMERROR;
+
+ zi->ci.crc32 = crc32(zi->ci.crc32,buf,(uInt)len);
+
+#ifdef HAVE_BZIP2
+ if(zi->ci.method == Z_BZIP2ED && (!zi->ci.raw))
+ {
+ zi->ci.bstream.next_in = (void*)buf;
+ zi->ci.bstream.avail_in = len;
+ err = BZ_RUN_OK;
+
+ while ((err==BZ_RUN_OK) && (zi->ci.bstream.avail_in>0))
+ {
+ if (zi->ci.bstream.avail_out == 0)
+ {
+ if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
+ err = ZIP_ERRNO;
+ zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;
+ zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;
+ }
+
+
+ if(err != BZ_RUN_OK)
+ break;
+
+ if ((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
+ {
+ uLong uTotalOutBefore_lo = zi->ci.bstream.total_out_lo32;
+// uLong uTotalOutBefore_hi = zi->ci.bstream.total_out_hi32;
+ err=BZ2_bzCompress(&zi->ci.bstream, BZ_RUN);
+
+ zi->ci.pos_in_buffered_data += (uInt)(zi->ci.bstream.total_out_lo32 - uTotalOutBefore_lo) ;
+ }
+ }
+
+ if(err == BZ_RUN_OK)
+ err = ZIP_OK;
+ }
+ else
+#endif
+ {
+ zi->ci.stream.next_in = (Bytef*)buf;
+ zi->ci.stream.avail_in = len;
+
+ while ((err==ZIP_OK) && (zi->ci.stream.avail_in>0))
+ {
+ if (zi->ci.stream.avail_out == 0)
+ {
+ if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
+ err = ZIP_ERRNO;
+ zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;
+ zi->ci.stream.next_out = zi->ci.buffered_data;
+ }
+
+
+ if(err != ZIP_OK)
+ break;
+
+ if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
+ {
+ uLong uTotalOutBefore = zi->ci.stream.total_out;
+ err=deflate(&zi->ci.stream, Z_NO_FLUSH);
+ if(uTotalOutBefore > zi->ci.stream.total_out)
+ {
+ int bBreak = 0;
+ bBreak++;
+ }
+
+ zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ;
+ }
+ else
+ {
+ uInt copy_this,i;
+ if (zi->ci.stream.avail_in < zi->ci.stream.avail_out)
+ copy_this = zi->ci.stream.avail_in;
+ else
+ copy_this = zi->ci.stream.avail_out;
+
+ for (i = 0; i < copy_this; i++)
+ *(((char*)zi->ci.stream.next_out)+i) =
+ *(((const char*)zi->ci.stream.next_in)+i);
+ {
+ zi->ci.stream.avail_in -= copy_this;
+ zi->ci.stream.avail_out-= copy_this;
+ zi->ci.stream.next_in+= copy_this;
+ zi->ci.stream.next_out+= copy_this;
+ zi->ci.stream.total_in+= copy_this;
+ zi->ci.stream.total_out+= copy_this;
+ zi->ci.pos_in_buffered_data += copy_this;
+ }
+ }
+ }// while(...)
+ }
+
+ return err;
+}
+
+extern int ZEXPORT zipCloseFileInZipRaw (zipFile file, uLong uncompressed_size, uLong crc32)
+{
+ return zipCloseFileInZipRaw64 (file, uncompressed_size, crc32);
+}
+
+extern int ZEXPORT zipCloseFileInZipRaw64 (zipFile file, ZPOS64_T uncompressed_size, uLong crc32)
+{
+ zip64_internal* zi;
+ ZPOS64_T compressed_size;
+ uLong invalidValue = 0xffffffff;
+ short datasize = 0;
+ int err=ZIP_OK;
+
+ if (file == NULL)
+ return ZIP_PARAMERROR;
+ zi = (zip64_internal*)file;
+
+ if (zi->in_opened_file_inzip == 0)
+ return ZIP_PARAMERROR;
+ zi->ci.stream.avail_in = 0;
+
+ if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
+ {
+ while (err==ZIP_OK)
+ {
+ uLong uTotalOutBefore;
+ if (zi->ci.stream.avail_out == 0)
+ {
+ if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
+ err = ZIP_ERRNO;
+ zi->ci.stream.avail_out = (uInt)Z_BUFSIZE;
+ zi->ci.stream.next_out = zi->ci.buffered_data;
+ }
+ uTotalOutBefore = zi->ci.stream.total_out;
+ err=deflate(&zi->ci.stream, Z_FINISH);
+ zi->ci.pos_in_buffered_data += (uInt)(zi->ci.stream.total_out - uTotalOutBefore) ;
+ }
+ }
+ else if ((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
+ {
+#ifdef HAVE_BZIP2
+ err = BZ_FINISH_OK;
+ while (err==BZ_FINISH_OK)
+ {
+ uLong uTotalOutBefore;
+ if (zi->ci.bstream.avail_out == 0)
+ {
+ if (zip64FlushWriteBuffer(zi) == ZIP_ERRNO)
+ err = ZIP_ERRNO;
+ zi->ci.bstream.avail_out = (uInt)Z_BUFSIZE;
+ zi->ci.bstream.next_out = (char*)zi->ci.buffered_data;
+ }
+ uTotalOutBefore = zi->ci.bstream.total_out_lo32;
+ err=BZ2_bzCompress(&zi->ci.bstream, BZ_FINISH);
+ if(err == BZ_STREAM_END)
+ err = Z_STREAM_END;
+
+ zi->ci.pos_in_buffered_data += (uInt)(zi->ci.bstream.total_out_lo32 - uTotalOutBefore);
+ }
+
+ if(err == BZ_FINISH_OK)
+ err = ZIP_OK;
+#endif
+ }
+
+ if (err==Z_STREAM_END)
+ err=ZIP_OK; /* this is normal */
+
+ if ((zi->ci.pos_in_buffered_data>0) && (err==ZIP_OK))
+ {
+ if (zip64FlushWriteBuffer(zi)==ZIP_ERRNO)
+ err = ZIP_ERRNO;
+ }
+
+ if ((zi->ci.method == Z_DEFLATED) && (!zi->ci.raw))
+ {
+ int tmp_err = deflateEnd(&zi->ci.stream);
+ if (err == ZIP_OK)
+ err = tmp_err;
+ zi->ci.stream_initialised = 0;
+ }
+#ifdef HAVE_BZIP2
+ else if((zi->ci.method == Z_BZIP2ED) && (!zi->ci.raw))
+ {
+ int tmperr = BZ2_bzCompressEnd(&zi->ci.bstream);
+ if (err==ZIP_OK)
+ err = tmperr;
+ zi->ci.stream_initialised = 0;
+ }
+#endif
+
+ if (!zi->ci.raw)
+ {
+ crc32 = (uLong)zi->ci.crc32;
+ uncompressed_size = zi->ci.totalUncompressedData;
+ }
+ compressed_size = zi->ci.totalCompressedData;
+
+ // update Current Item crc and sizes,
+ if(compressed_size >= 0xffffffff || uncompressed_size >= 0xffffffff || zi->ci.pos_local_header >= 0xffffffff)
+ {
+ /*version Made by*/
+ zip64local_putValue_inmemory(zi->ci.central_header+4,(uLong)45,2);
+ /*version needed*/
+ zip64local_putValue_inmemory(zi->ci.central_header+6,(uLong)45,2);
+
+ }
+
+ zip64local_putValue_inmemory(zi->ci.central_header+16,crc32,4); /*crc*/
+
+
+ if(compressed_size >= 0xffffffff)
+ zip64local_putValue_inmemory(zi->ci.central_header+20, invalidValue,4); /*compr size*/
+ else
+ zip64local_putValue_inmemory(zi->ci.central_header+20, compressed_size,4); /*compr size*/
+
+ /// set internal file attributes field
+ if (zi->ci.stream.data_type == Z_ASCII)
+ zip64local_putValue_inmemory(zi->ci.central_header+36,(uLong)Z_ASCII,2);
+
+ if(uncompressed_size >= 0xffffffff)
+ zip64local_putValue_inmemory(zi->ci.central_header+24, invalidValue,4); /*uncompr size*/
+ else
+ zip64local_putValue_inmemory(zi->ci.central_header+24, uncompressed_size,4); /*uncompr size*/
+
+ // Add ZIP64 extra info field for uncompressed size
+ if(uncompressed_size >= 0xffffffff)
+ datasize += 8;
+
+ // Add ZIP64 extra info field for compressed size
+ if(compressed_size >= 0xffffffff)
+ datasize += 8;
+
+ // Add ZIP64 extra info field for relative offset to local file header of current file
+ if(zi->ci.pos_local_header >= 0xffffffff)
+ datasize += 8;
+
+ if(datasize > 0)
+ {
+ char* p = NULL;
+
+ if((uLong)(datasize + 4) > zi->ci.size_centralExtraFree)
+ {
+ // we can not write more data to the buffer that we have room for.
+ return ZIP_BADZIPFILE;
+ }
+
+ p = zi->ci.central_header + zi->ci.size_centralheader;
+
+ // Add Extra Information Header for 'ZIP64 information'
+ zip64local_putValue_inmemory(p, 0x0001, 2); // HeaderID
+ p += 2;
+ zip64local_putValue_inmemory(p, datasize, 2); // DataSize
+ p += 2;
+
+ if(uncompressed_size >= 0xffffffff)
+ {
+ zip64local_putValue_inmemory(p, uncompressed_size, 8);
+ p += 8;
+ }
+
+ if(compressed_size >= 0xffffffff)
+ {
+ zip64local_putValue_inmemory(p, compressed_size, 8);
+ p += 8;
+ }
+
+ if(zi->ci.pos_local_header >= 0xffffffff)
+ {
+ zip64local_putValue_inmemory(p, zi->ci.pos_local_header, 8);
+ p += 8;
+ }
+
+ // Update how much extra free space we got in the memory buffer
+ // and increase the centralheader size so the new ZIP64 fields are included
+ // ( 4 below is the size of HeaderID and DataSize field )
+ zi->ci.size_centralExtraFree -= datasize + 4;
+ zi->ci.size_centralheader += datasize + 4;
+
+ // Update the extra info size field
+ zi->ci.size_centralExtra += datasize + 4;
+ zip64local_putValue_inmemory(zi->ci.central_header+30,(uLong)zi->ci.size_centralExtra,2);
+ }
+
+ if (err==ZIP_OK)
+ err = add_data_in_datablock(&zi->central_dir, zi->ci.central_header, (uLong)zi->ci.size_centralheader);
+
+ free(zi->ci.central_header);
+
+ if (err==ZIP_OK)
+ {
+ // Update the LocalFileHeader with the new values.
+
+ ZPOS64_T cur_pos_inzip = ZTELL64(zi->z_filefunc,zi->filestream);
+
+ if (ZSEEK64(zi->z_filefunc,zi->filestream, zi->ci.pos_local_header + 14,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ err = ZIP_ERRNO;
+
+ if (err==ZIP_OK)
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,crc32,4); /* crc 32, unknown */
+
+ if(uncompressed_size >= 0xffffffff || compressed_size >= 0xffffffff )
+ {
+ if(zi->ci.pos_zip64extrainfo > 0)
+ {
+ // Update the size in the ZIP64 extended field.
+ if (ZSEEK64(zi->z_filefunc,zi->filestream, zi->ci.pos_zip64extrainfo + 4,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ err = ZIP_ERRNO;
+
+ if (err==ZIP_OK) /* compressed size, unknown */
+ err = zip64local_putValue(&zi->z_filefunc, zi->filestream, uncompressed_size, 8);
+
+ if (err==ZIP_OK) /* uncompressed size, unknown */
+ err = zip64local_putValue(&zi->z_filefunc, zi->filestream, compressed_size, 8);
+ }
+ else
+ err = ZIP_BADZIPFILE; // Caller passed zip64 = 0, so no room for zip64 info -> fatal
+ }
+ else
+ {
+ if (err==ZIP_OK) /* compressed size, unknown */
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,compressed_size,4);
+
+ if (err==ZIP_OK) /* uncompressed size, unknown */
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,uncompressed_size,4);
+ }
+
+ if (ZSEEK64(zi->z_filefunc,zi->filestream, cur_pos_inzip,ZLIB_FILEFUNC_SEEK_SET)!=0)
+ err = ZIP_ERRNO;
+ }
+
+ zi->number_entry ++;
+ zi->in_opened_file_inzip = 0;
+
+ return err;
+}
+
+extern int ZEXPORT zipCloseFileInZip (zipFile file)
+{
+ return zipCloseFileInZipRaw (file,0,0);
+}
+
+int Write_Zip64EndOfCentralDirectoryLocator(zip64_internal* zi, ZPOS64_T zip64eocd_pos_inzip)
+{
+ int err = ZIP_OK;
+ ZPOS64_T pos = zip64eocd_pos_inzip - zi->add_position_when_writing_offset;
+
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ZIP64ENDLOCHEADERMAGIC,4);
+
+ /*num disks*/
+ if (err==ZIP_OK) /* number of the disk with the start of the central directory */
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);
+
+ /*relative offset*/
+ if (err==ZIP_OK) /* Relative offset to the Zip64EndOfCentralDirectory */
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream, pos,8);
+
+ /*total disks*/ /* Do not support spawning of disk so always say 1 here*/
+ if (err==ZIP_OK) /* number of the disk with the start of the central directory */
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)1,4);
+
+ return err;
+}
+
+int Write_Zip64EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip)
+{
+ int err = ZIP_OK;
+
+ uLong Zip64DataSize = 44;
+
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ZIP64ENDHEADERMAGIC,4);
+
+ if (err==ZIP_OK) /* size of this 'zip64 end of central directory' */
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(ZPOS64_T)Zip64DataSize,8); // why ZPOS64_T of this ?
+
+ if (err==ZIP_OK) /* version made by */
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);
+
+ if (err==ZIP_OK) /* version needed */
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)45,2);
+
+ if (err==ZIP_OK) /* number of this disk */
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);
+
+ if (err==ZIP_OK) /* number of the disk with the start of the central directory */
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,4);
+
+ if (err==ZIP_OK) /* total number of entries in the central dir on this disk */
+ err = zip64local_putValue(&zi->z_filefunc, zi->filestream, zi->number_entry, 8);
+
+ if (err==ZIP_OK) /* total number of entries in the central dir */
+ err = zip64local_putValue(&zi->z_filefunc, zi->filestream, zi->number_entry, 8);
+
+ if (err==ZIP_OK) /* size of the central directory */
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(ZPOS64_T)size_centraldir,8);
+
+ if (err==ZIP_OK) /* offset of start of central directory with respect to the starting disk number */
+ {
+ ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (ZPOS64_T)pos,8);
+ }
+ return err;
+}
+int Write_EndOfCentralDirectoryRecord(zip64_internal* zi, uLong size_centraldir, ZPOS64_T centraldir_pos_inzip)
+{
+ int err = ZIP_OK;
+
+ /*signature*/
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)ENDHEADERMAGIC,4);
+
+ if (err==ZIP_OK) /* number of this disk */
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,2);
+
+ if (err==ZIP_OK) /* number of the disk with the start of the central directory */
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0,2);
+
+ if (err==ZIP_OK) /* total number of entries in the central dir on this disk */
+ {
+ {
+ if(zi->number_entry >= 0xFFFF)
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xffff,2); // use value in ZIP64 record
+ else
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->number_entry,2);
+ }
+ }
+
+ if (err==ZIP_OK) /* total number of entries in the central dir */
+ {
+ if(zi->number_entry >= 0xFFFF)
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)0xffff,2); // use value in ZIP64 record
+ else
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)zi->number_entry,2);
+ }
+
+ if (err==ZIP_OK) /* size of the central directory */
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_centraldir,4);
+
+ if (err==ZIP_OK) /* offset of start of central directory with respect to the starting disk number */
+ {
+ ZPOS64_T pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;
+ if(pos >= 0xffffffff)
+ {
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)0xffffffff,4);
+ }
+ else
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream, (uLong)(centraldir_pos_inzip - zi->add_position_when_writing_offset),4);
+ }
+
+ return err;
+}
+
+int Write_GlobalComment(zip64_internal* zi, const char* global_comment)
+{
+ int err = ZIP_OK;
+ uInt size_global_comment = 0;
+
+ if(global_comment != NULL)
+ size_global_comment = (uInt)strlen(global_comment);
+
+ err = zip64local_putValue(&zi->z_filefunc,zi->filestream,(uLong)size_global_comment,2);
+
+ if (err == ZIP_OK && size_global_comment > 0)
+ {
+ if (ZWRITE64(zi->z_filefunc,zi->filestream, global_comment, size_global_comment) != size_global_comment)
+ err = ZIP_ERRNO;
+ }
+ return err;
+}
+
+extern int ZEXPORT zipClose (zipFile file, const char* global_comment)
+{
+ zip64_internal* zi;
+ int err = 0;
+ uLong size_centraldir = 0;
+ ZPOS64_T centraldir_pos_inzip;
+ ZPOS64_T pos;
+
+ if (file == NULL)
+ return ZIP_PARAMERROR;
+
+ zi = (zip64_internal*)file;
+
+ if (zi->in_opened_file_inzip == 1)
+ {
+ err = zipCloseFileInZip (file);
+ }
+
+#ifndef NO_ADDFILEINEXISTINGZIP
+ if (global_comment==NULL)
+ global_comment = zi->globalcomment;
+#endif
+
+ centraldir_pos_inzip = ZTELL64(zi->z_filefunc,zi->filestream);
+
+ if (err==ZIP_OK)
+ {
+ linkedlist_datablock_internal* ldi = zi->central_dir.first_block;
+ while (ldi!=NULL)
+ {
+ if ((err==ZIP_OK) && (ldi->filled_in_this_block>0))
+ {
+ if (ZWRITE64(zi->z_filefunc,zi->filestream, ldi->data, ldi->filled_in_this_block) != ldi->filled_in_this_block)
+ err = ZIP_ERRNO;
+ }
+
+ size_centraldir += ldi->filled_in_this_block;
+ ldi = ldi->next_datablock;
+ }
+ }
+ free_linkedlist(&(zi->central_dir));
+
+ pos = centraldir_pos_inzip - zi->add_position_when_writing_offset;
+ if(pos >= 0xffffffff || zi->number_entry > 0xFFFF)
+ {
+ ZPOS64_T Zip64EOCDpos = ZTELL64(zi->z_filefunc,zi->filestream);
+ Write_Zip64EndOfCentralDirectoryRecord(zi, size_centraldir, centraldir_pos_inzip);
+
+ Write_Zip64EndOfCentralDirectoryLocator(zi, Zip64EOCDpos);
+ }
+
+ if (err==ZIP_OK)
+ err = Write_EndOfCentralDirectoryRecord(zi, size_centraldir, centraldir_pos_inzip);
+
+ if(err == ZIP_OK)
+ err = Write_GlobalComment(zi, global_comment);
+
+ if (ZCLOSE64(zi->z_filefunc,zi->filestream) != 0)
+ if (err == ZIP_OK)
+ err = ZIP_ERRNO;
+
+#ifndef NO_ADDFILEINEXISTINGZIP
+ TRYFREE(zi->globalcomment);
+#endif
+ TRYFREE(zi);
+
+ return err;
+}
+
+extern int ZEXPORT zipRemoveExtraInfoBlock (char* pData, int* dataLen, short sHeader)
+{
+ char* p = pData;
+ int size = 0;
+ char* pNewHeader;
+ char* pTmp;
+ short header;
+ short dataSize;
+
+ int retVal = ZIP_OK;
+
+ if(pData == NULL || *dataLen < 4)
+ return ZIP_PARAMERROR;
+
+ pNewHeader = (char*)ALLOC(*dataLen);
+ pTmp = pNewHeader;
+
+ while(p < (pData + *dataLen))
+ {
+ header = *(short*)p;
+ dataSize = *(((short*)p)+1);
+
+ if( header == sHeader ) // Header found.
+ {
+ p += dataSize + 4; // skip it. do not copy to temp buffer
+ }
+ else
+ {
+ // Extra Info block should not be removed, So copy it to the temp buffer.
+ memcpy(pTmp, p, dataSize + 4);
+ p += dataSize + 4;
+ size += dataSize + 4;
+ }
+
+ }
+
+ if(size < *dataLen)
+ {
+ // clean old extra info block.
+ memset(pData,0, *dataLen);
+
+ // copy the new extra info block over the old
+ if(size > 0)
+ memcpy(pData, pNewHeader, size);
+
+ // set the new extra info size
+ *dataLen = size;
+
+ retVal = ZIP_OK;
+ }
+ else
+ retVal = ZIP_ERRNO;
+
+ TRYFREE(pNewHeader);
+
+ return retVal;
+}
diff --git a/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/zip.h b/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/zip.h
new file mode 100644
index 0000000..8aaebb6
--- /dev/null
+++ b/Sources/WireGuardApp/ZipArchive/3rdparty/minizip/zip.h
@@ -0,0 +1,362 @@
+/* zip.h -- IO on .zip files using zlib
+ Version 1.1, February 14h, 2010
+ part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html )
+
+ Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html )
+
+ Modifications for Zip64 support
+ Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com )
+
+ For more info read MiniZip_info.txt
+
+ ---------------------------------------------------------------------------
+
+ Condition of use and distribution are the same than zlib :
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+
+ ---------------------------------------------------------------------------
+
+ Changes
+
+ See header of zip.h
+
+*/
+
+#ifndef _zip12_H
+#define _zip12_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+//#define HAVE_BZIP2
+
+#ifndef _ZLIB_H
+#include "zlib.h"
+#endif
+
+#ifndef _ZLIBIOAPI_H
+#include "ioapi.h"
+#endif
+
+#ifdef HAVE_BZIP2
+#include "bzlib.h"
+#endif
+
+#define Z_BZIP2ED 12
+
+#if defined(STRICTZIP) || defined(STRICTZIPUNZIP)
+/* like the STRICT of WIN32, we define a pointer that cannot be converted
+ from (void*) without cast */
+typedef struct TagzipFile__ { int unused; } zipFile__;
+typedef zipFile__ *zipFile;
+#else
+typedef voidp zipFile;
+#endif
+
+#define ZIP_OK (0)
+#define ZIP_EOF (0)
+#define ZIP_ERRNO (Z_ERRNO)
+#define ZIP_PARAMERROR (-102)
+#define ZIP_BADZIPFILE (-103)
+#define ZIP_INTERNALERROR (-104)
+
+#ifndef DEF_MEM_LEVEL
+# if MAX_MEM_LEVEL >= 8
+# define DEF_MEM_LEVEL 8
+# else
+# define DEF_MEM_LEVEL MAX_MEM_LEVEL
+# endif
+#endif
+/* default memLevel */
+
+/* tm_zip contain date/time info */
+typedef struct tm_zip_s
+{
+ uInt tm_sec; /* seconds after the minute - [0,59] */
+ uInt tm_min; /* minutes after the hour - [0,59] */
+ uInt tm_hour; /* hours since midnight - [0,23] */
+ uInt tm_mday; /* day of the month - [1,31] */
+ uInt tm_mon; /* months since January - [0,11] */
+ uInt tm_year; /* years - [1980..2044] */
+} tm_zip;
+
+typedef struct
+{
+ tm_zip tmz_date; /* date in understandable format */
+ uLong dosDate; /* if dos_date == 0, tmu_date is used */
+/* uLong flag; */ /* general purpose bit flag 2 bytes */
+
+ uLong internal_fa; /* internal file attributes 2 bytes */
+ uLong external_fa; /* external file attributes 4 bytes */
+} zip_fileinfo;
+
+typedef const char* zipcharpc;
+
+
+#define APPEND_STATUS_CREATE (0)
+#define APPEND_STATUS_CREATEAFTER (1)
+#define APPEND_STATUS_ADDINZIP (2)
+
+extern zipFile ZEXPORT zipOpen OF((const char *pathname, int append));
+extern zipFile ZEXPORT zipOpen64 OF((const void *pathname, int append));
+/*
+ Create a zipfile.
+ pathname contain on Windows XP a filename like "c:\\zlib\\zlib113.zip" or on
+ an Unix computer "zlib/zlib113.zip".
+ if the file pathname exist and append==APPEND_STATUS_CREATEAFTER, the zip
+ will be created at the end of the file.
+ (useful if the file contain a self extractor code)
+ if the file pathname exist and append==APPEND_STATUS_ADDINZIP, we will
+ add files in existing zip (be sure you don't add file that doesn't exist)
+ If the zipfile cannot be opened, the return value is NULL.
+ Else, the return value is a zipFile Handle, usable with other function
+ of this zip package.
+*/
+
+/* Note : there is no delete function into a zipfile.
+ If you want delete file into a zipfile, you must open a zipfile, and create another
+ Of couse, you can use RAW reading and writing to copy the file you did not want delte
+*/
+
+extern zipFile ZEXPORT zipOpen2 OF((const char *pathname,
+ int append,
+ zipcharpc* globalcomment,
+ zlib_filefunc_def* pzlib_filefunc_def));
+
+extern zipFile ZEXPORT zipOpen2_64 OF((const void *pathname,
+ int append,
+ zipcharpc* globalcomment,
+ zlib_filefunc64_def* pzlib_filefunc_def));
+
+extern int ZEXPORT zipOpenNewFileInZip OF((zipFile file,
+ const char* filename,
+ const zip_fileinfo* zipfi,
+ const void* extrafield_local,
+ uInt size_extrafield_local,
+ const void* extrafield_global,
+ uInt size_extrafield_global,
+ const char* comment,
+ int method,
+ int level));
+
+extern int ZEXPORT zipOpenNewFileInZip64 OF((zipFile file,
+ const char* filename,
+ const zip_fileinfo* zipfi,
+ const void* extrafield_local,
+ uInt size_extrafield_local,
+ const void* extrafield_global,
+ uInt size_extrafield_global,
+ const char* comment,
+ int method,
+ int level,
+ int zip64));
+
+/*
+ Open a file in the ZIP for writing.
+ filename : the filename in zip (if NULL, '-' without quote will be used
+ *zipfi contain supplemental information
+ if extrafield_local!=NULL and size_extrafield_local>0, extrafield_local
+ contains the extrafield data the the local header
+ if extrafield_global!=NULL and size_extrafield_global>0, extrafield_global
+ contains the extrafield data the the local header
+ if comment != NULL, comment contain the comment string
+ method contain the compression method (0 for store, Z_DEFLATED for deflate)
+ level contain the level of compression (can be Z_DEFAULT_COMPRESSION)
+ zip64 is set to 1 if a zip64 extended information block should be added to the local file header.
+ this MUST be '1' if the uncompressed size is >= 0xffffffff.
+
+*/
+
+
+extern int ZEXPORT zipOpenNewFileInZip2 OF((zipFile file,
+ const char* filename,
+ const zip_fileinfo* zipfi,
+ const void* extrafield_local,
+ uInt size_extrafield_local,
+ const void* extrafield_global,
+ uInt size_extrafield_global,
+ const char* comment,
+ int method,
+ int level,
+ int raw));
+
+
+extern int ZEXPORT zipOpenNewFileInZip2_64 OF((zipFile file,
+ const char* filename,
+ const zip_fileinfo* zipfi,
+ const void* extrafield_local,
+ uInt size_extrafield_local,
+ const void* extrafield_global,
+ uInt size_extrafield_global,
+ const char* comment,
+ int method,
+ int level,
+ int raw,
+ int zip64));
+/*
+ Same than zipOpenNewFileInZip, except if raw=1, we write raw file
+ */
+
+extern int ZEXPORT zipOpenNewFileInZip3 OF((zipFile file,
+ const char* filename,
+ const zip_fileinfo* zipfi,
+ const void* extrafield_local,
+ uInt size_extrafield_local,
+ const void* extrafield_global,
+ uInt size_extrafield_global,
+ const char* comment,
+ int method,
+ int level,
+ int raw,
+ int windowBits,
+ int memLevel,
+ int strategy,
+ const char* password,
+ uLong crcForCrypting));
+
+extern int ZEXPORT zipOpenNewFileInZip3_64 OF((zipFile file,
+ const char* filename,
+ const zip_fileinfo* zipfi,
+ const void* extrafield_local,
+ uInt size_extrafield_local,
+ const void* extrafield_global,
+ uInt size_extrafield_global,
+ const char* comment,
+ int method,
+ int level,
+ int raw,
+ int windowBits,
+ int memLevel,
+ int strategy,
+ const char* password,
+ uLong crcForCrypting,
+ int zip64
+ ));
+
+/*
+ Same than zipOpenNewFileInZip2, except
+ windowBits,memLevel,,strategy : see parameter strategy in deflateInit2
+ password : crypting password (NULL for no crypting)
+ crcForCrypting : crc of file to compress (needed for crypting)
+ */
+
+extern int ZEXPORT zipOpenNewFileInZip4 OF((zipFile file,
+ const char* filename,
+ const zip_fileinfo* zipfi,
+ const void* extrafield_local,
+ uInt size_extrafield_local,
+ const void* extrafield_global,
+ uInt size_extrafield_global,
+ const char* comment,
+ int method,
+ int level,
+ int raw,
+ int windowBits,
+ int memLevel,
+ int strategy,
+ const char* password,
+ uLong crcForCrypting,
+ uLong versionMadeBy,
+ uLong flagBase
+ ));
+
+
+extern int ZEXPORT zipOpenNewFileInZip4_64 OF((zipFile file,
+ const char* filename,
+ const zip_fileinfo* zipfi,
+ const void* extrafield_local,
+ uInt size_extrafield_local,
+ const void* extrafield_global,
+ uInt size_extrafield_global,
+ const char* comment,
+ int method,
+ int level,
+ int raw,
+ int windowBits,
+ int memLevel,
+ int strategy,
+ const char* password,
+ uLong crcForCrypting,
+ uLong versionMadeBy,
+ uLong flagBase,
+ int zip64
+ ));
+/*
+ Same than zipOpenNewFileInZip4, except
+ versionMadeBy : value for Version made by field
+ flag : value for flag field (compression level info will be added)
+ */
+
+
+extern int ZEXPORT zipWriteInFileInZip OF((zipFile file,
+ const void* buf,
+ unsigned len));
+/*
+ Write data in the zipfile
+*/
+
+extern int ZEXPORT zipCloseFileInZip OF((zipFile file));
+/*
+ Close the current file in the zipfile
+*/
+
+extern int ZEXPORT zipCloseFileInZipRaw OF((zipFile file,
+ uLong uncompressed_size,
+ uLong crc32));
+
+extern int ZEXPORT zipCloseFileInZipRaw64 OF((zipFile file,
+ ZPOS64_T uncompressed_size,
+ uLong crc32));
+
+/*
+ Close the current file in the zipfile, for file opened with
+ parameter raw=1 in zipOpenNewFileInZip2
+ uncompressed_size and crc32 are value for the uncompressed size
+*/
+
+extern int ZEXPORT zipClose OF((zipFile file,
+ const char* global_comment));
+/*
+ Close the zipfile
+*/
+
+
+extern int ZEXPORT zipRemoveExtraInfoBlock OF((char* pData, int* dataLen, short sHeader));
+/*
+ zipRemoveExtraInfoBlock - Added by Mathias Svensson
+
+ Remove extra information block from a extra information data for the local file header or central directory header
+
+ It is needed to remove ZIP64 extra information blocks when before data is written if using RAW mode.
+
+ 0x0001 is the signature header for the ZIP64 extra information blocks
+
+ usage.
+ Remove ZIP64 Extra information from a central director extra field data
+ zipRemoveExtraInfoBlock(pCenDirExtraFieldData, &nCenDirExtraFieldDataLen, 0x0001);
+
+ Remove ZIP64 Extra information from a Local File Header extra field data
+ zipRemoveExtraInfoBlock(pLocalHeaderExtraFieldData, &nLocalHeaderExtraFieldDataLen, 0x0001);
+*/
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _zip64_H */
diff --git a/Sources/WireGuardApp/ZipArchive/ZipArchive.swift b/Sources/WireGuardApp/ZipArchive/ZipArchive.swift
new file mode 100644
index 0000000..9c2f634
--- /dev/null
+++ b/Sources/WireGuardApp/ZipArchive/ZipArchive.swift
@@ -0,0 +1,105 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+
+enum ZipArchiveError: WireGuardAppError {
+ case cantOpenInputZipFile
+ case cantOpenOutputZipFileForWriting
+ case badArchive
+ case noTunnelsInZipArchive
+
+ var alertText: AlertText {
+ switch self {
+ case .cantOpenInputZipFile:
+ return (tr("alertCantOpenInputZipFileTitle"), tr("alertCantOpenInputZipFileMessage"))
+ case .cantOpenOutputZipFileForWriting:
+ return (tr("alertCantOpenOutputZipFileForWritingTitle"), tr("alertCantOpenOutputZipFileForWritingMessage"))
+ case .badArchive:
+ return (tr("alertBadArchiveTitle"), tr("alertBadArchiveMessage"))
+ case .noTunnelsInZipArchive:
+ return (tr("alertNoTunnelsInImportedZipArchiveTitle"), tr("alertNoTunnelsInImportedZipArchiveMessage"))
+ }
+ }
+}
+
+enum ZipArchive {}
+
+extension ZipArchive {
+
+ static func archive(inputs: [(fileName: String, contents: Data)], to destinationURL: URL) throws {
+ let destinationPath = destinationURL.path
+ guard let zipFile = zipOpen(destinationPath, APPEND_STATUS_CREATE) else {
+ throw ZipArchiveError.cantOpenOutputZipFileForWriting
+ }
+ for input in inputs {
+ let fileName = input.fileName
+ let contents = input.contents
+ zipOpenNewFileInZip(zipFile, fileName.cString(using: .utf8), nil, nil, 0, nil, 0, nil, Z_DEFLATED, Z_DEFAULT_COMPRESSION)
+ contents.withUnsafeBytes { rawBufferPointer -> Void in
+ zipWriteInFileInZip(zipFile, rawBufferPointer.baseAddress, UInt32(contents.count))
+ }
+ zipCloseFileInZip(zipFile)
+ }
+ zipClose(zipFile, nil)
+ }
+
+ static func unarchive(url: URL, requiredFileExtensions: [String]) throws -> [(fileBaseName: String, contents: Data)] {
+
+ var results = [(fileBaseName: String, contents: Data)]()
+ var requiredFileExtensionsLowercased = requiredFileExtensions.map { $0.lowercased() }
+
+ guard let zipFile = unzOpen64(url.path) else {
+ throw ZipArchiveError.cantOpenInputZipFile
+ }
+ defer {
+ unzClose(zipFile)
+ }
+ guard unzGoToFirstFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
+
+ var resultOfGoToNextFile: Int32
+ repeat {
+ guard unzOpenCurrentFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
+
+ let bufferSize = 16384 // 16 KiB
+ var fileNameBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
+ var dataBuffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferSize)
+
+ defer {
+ fileNameBuffer.deallocate()
+ dataBuffer.deallocate()
+ }
+
+ guard unzGetCurrentFileInfo64(zipFile, nil, fileNameBuffer, UInt(bufferSize), nil, 0, nil, 0) == UNZ_OK else { throw ZipArchiveError.badArchive }
+
+ let lastChar = String(cString: fileNameBuffer).suffix(1)
+ let isDirectory = (lastChar == "/" || lastChar == "\\")
+ let fileURL = URL(fileURLWithFileSystemRepresentation: fileNameBuffer, isDirectory: isDirectory, relativeTo: nil)
+
+ if !isDirectory && requiredFileExtensionsLowercased.contains(fileURL.pathExtension.lowercased()) {
+ var unzippedData = Data()
+ var bytesRead: Int32 = 0
+ repeat {
+ bytesRead = unzReadCurrentFile(zipFile, dataBuffer, UInt32(bufferSize))
+ if bytesRead > 0 {
+ let dataRead = dataBuffer.withMemoryRebound(to: UInt8.self, capacity: bufferSize) {
+ return Data(bytes: $0, count: Int(bytesRead))
+ }
+ unzippedData.append(dataRead)
+ }
+ } while bytesRead > 0
+ results.append((fileBaseName: fileURL.deletingPathExtension().lastPathComponent, contents: unzippedData))
+ }
+
+ guard unzCloseCurrentFile(zipFile) == UNZ_OK else { throw ZipArchiveError.badArchive }
+
+ resultOfGoToNextFile = unzGoToNextFile(zipFile)
+ } while resultOfGoToNextFile == UNZ_OK
+
+ if resultOfGoToNextFile == UNZ_END_OF_LIST_OF_FILE {
+ return results
+ } else {
+ throw ZipArchiveError.badArchive
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/ZipArchive/ZipExporter.swift b/Sources/WireGuardApp/ZipArchive/ZipExporter.swift
new file mode 100644
index 0000000..fe3d984
--- /dev/null
+++ b/Sources/WireGuardApp/ZipArchive/ZipExporter.swift
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+import WireGuardKit
+
+enum ZipExporterError: WireGuardAppError {
+ case noTunnelsToExport
+
+ var alertText: AlertText {
+ return (tr("alertNoTunnelsToExportTitle"), tr("alertNoTunnelsToExportMessage"))
+ }
+}
+
+class ZipExporter {
+ static func exportConfigFiles(tunnelConfigurations: [TunnelConfiguration], to url: URL, completion: @escaping (WireGuardAppError?) -> Void) {
+
+ guard !tunnelConfigurations.isEmpty else {
+ completion(ZipExporterError.noTunnelsToExport)
+ return
+ }
+ DispatchQueue.global(qos: .userInitiated).async {
+ var inputsToArchiver: [(fileName: String, contents: Data)] = []
+ var lastTunnelName: String = ""
+ for tunnelConfiguration in tunnelConfigurations {
+ if let contents = tunnelConfiguration.asWgQuickConfig().data(using: .utf8) {
+ let name = tunnelConfiguration.name ?? "untitled"
+ if name.isEmpty || name == lastTunnelName { continue }
+ inputsToArchiver.append((fileName: "\(name).conf", contents: contents))
+ lastTunnelName = name
+ }
+ }
+ do {
+ try ZipArchive.archive(inputs: inputsToArchiver, to: url)
+ } catch let error as WireGuardAppError {
+ DispatchQueue.main.async { completion(error) }
+ return
+ } catch {
+ fatalError()
+ }
+ DispatchQueue.main.async { completion(nil) }
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/ZipArchive/ZipImporter.swift b/Sources/WireGuardApp/ZipArchive/ZipImporter.swift
new file mode 100644
index 0000000..c988e94
--- /dev/null
+++ b/Sources/WireGuardApp/ZipArchive/ZipImporter.swift
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+
+import Foundation
+import WireGuardKit
+
+class ZipImporter {
+ static func importConfigFiles(from url: URL, completion: @escaping (Result<[TunnelConfiguration?], ZipArchiveError>) -> Void) {
+ DispatchQueue.global(qos: .userInitiated).async {
+ var unarchivedFiles: [(fileBaseName: String, contents: Data)]
+ do {
+ unarchivedFiles = try ZipArchive.unarchive(url: url, requiredFileExtensions: ["conf"])
+ for (index, unarchivedFile) in unarchivedFiles.enumerated().reversed() {
+ let fileBaseName = unarchivedFile.fileBaseName
+ let trimmedName = fileBaseName.trimmingCharacters(in: .whitespacesAndNewlines)
+ if !trimmedName.isEmpty {
+ unarchivedFiles[index].fileBaseName = trimmedName
+ } else {
+ unarchivedFiles.remove(at: index)
+ }
+ }
+
+ if unarchivedFiles.isEmpty {
+ throw ZipArchiveError.noTunnelsInZipArchive
+ }
+ } catch let error as ZipArchiveError {
+ DispatchQueue.main.async { completion(.failure(error)) }
+ return
+ } catch {
+ fatalError()
+ }
+
+ unarchivedFiles.sort { TunnelsManager.tunnelNameIsLessThan($0.fileBaseName, $1.fileBaseName) }
+ var configs: [TunnelConfiguration?] = Array(repeating: nil, count: unarchivedFiles.count)
+ for (index, file) in unarchivedFiles.enumerated() {
+ if index > 0 && file == unarchivedFiles[index - 1] {
+ continue
+ }
+ guard let fileContents = String(data: file.contents, encoding: .utf8) else { continue }
+ guard let tunnelConfig = try? TunnelConfiguration(fromWgQuickConfig: fileContents, called: file.fileBaseName) else { continue }
+ configs[index] = tunnelConfig
+ }
+ DispatchQueue.main.async { completion(.success(configs)) }
+ }
+ }
+}
diff --git a/Sources/WireGuardApp/de.lproj/Localizable.strings b/Sources/WireGuardApp/de.lproj/Localizable.strings
new file mode 100644
index 0000000..5bb0db4
--- /dev/null
+++ b/Sources/WireGuardApp/de.lproj/Localizable.strings
@@ -0,0 +1,445 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
+
+// Generic alert action names
+
+"actionOK" = "OK";
+"actionCancel" = "Abbrechen";
+"actionSave" = "Speichern";
+
+// Tunnels list UI
+
+"tunnelsListTitle" = "WireGuard";
+"tunnelsListSettingsButtonTitle" = "Einstellungen";
+"tunnelsListCenteredAddTunnelButtonTitle" = "Tunnel hinzufügen";
+"tunnelsListSwipeDeleteButtonTitle" = "Entfernen";
+"tunnelsListSelectButtonTitle" = "Auswählen";
+"tunnelsListSelectAllButtonTitle" = "Alle Auswählen";
+"tunnelsListDeleteButtonTitle" = "Entfernen";
+"tunnelsListSelectedTitle (%d)" = "%d ausgewählt";
+
+// Tunnels list menu
+
+"addTunnelMenuHeader" = "WireGuard-Tunnel hinzufügen";
+"addTunnelMenuImportFile" = "Aus Datei oder Archiv erstellen";
+"addTunnelMenuQRCode" = "Aus QR-Code erstellen";
+"addTunnelMenuFromScratch" = "Selbst erstellen";
+
+// Tunnels list alerts
+
+"alertImportedFromMultipleFilesTitle (%d)" = "%d Tunnel erstellt";
+"alertImportedFromMultipleFilesMessage (%1$d of %2$d)" = "%1$d von %2$d Tunnel aus importierten Dateien erstellt";
+
+"alertImportedFromZipTitle (%d)" = "%d Tunnel erstellt";
+"alertImportedFromZipMessage (%1$d of %2$d)" = "%1$d von %2$d Tunnel aus Zip-Archiv erstellt";
+
+"alertBadConfigImportTitle" = "Tunnel konnte nicht importiert werden";
+"alertBadConfigImportMessage (%@)" = "Die Datei ‘%@’ enthält keine gültige WireGuard-Konfiguration";
+
+"deleteTunnelsConfirmationAlertButtonTitle" = "Entfernen";
+"deleteTunnelConfirmationAlertButtonMessage (%d)" = "%d Tunnel löschen?";
+"deleteTunnelsConfirmationAlertButtonMessage (%d)" = "%d Tunnel löschen?";
+
+// Tunnel detail and edit UI
+
+"newTunnelViewTitle" = "Neue Konfiguration";
+"editTunnelViewTitle" = "Konfiguration bearbeiten";
+
+"tunnelSectionTitleStatus" = "Status";
+
+"tunnelStatusInactive" = "Inaktiv";
+"tunnelStatusActivating" = "Aktiviere";
+"tunnelStatusActive" = "Aktiv";
+"tunnelStatusDeactivating" = "Deaktiviere";
+"tunnelStatusReasserting" = "Reaktiviere";
+"tunnelStatusRestarting" = "Starte neu";
+"tunnelStatusWaiting" = "Warten";
+
+"macToggleStatusButtonActivate" = "Aktiviere";
+"macToggleStatusButtonActivating" = "Aktiviere…";
+"macToggleStatusButtonDeactivate" = "Deaktiviere";
+"macToggleStatusButtonDeactivating" = "Deaktiviere…";
+"macToggleStatusButtonReasserting" = "Reaktiviere…";
+"macToggleStatusButtonRestarting" = "Starte neu…";
+"macToggleStatusButtonWaiting" = "Warten…";
+
+"tunnelSectionTitleInterface" = "Schnittstelle";
+
+"tunnelInterfaceName" = "Name";
+"tunnelInterfacePrivateKey" = "Privater Schlüssel";
+"tunnelInterfacePublicKey" = "Öffentlicher Schlüssel";
+"tunnelInterfaceGenerateKeypair" = "Schlüsselpaar erzeugen";
+"tunnelInterfaceAddresses" = "Adressen";
+"tunnelInterfaceListenPort" = "Zu überwachender Port";
+"tunnelInterfaceMTU" = "MTU";
+"tunnelInterfaceDNS" = "DNS-Server";
+"tunnelInterfaceStatus" = "Status";
+
+"tunnelSectionTitlePeer" = "Peer";
+
+"tunnelPeerPublicKey" = "Öffentlicher Schlüssel";
+"tunnelPeerPreSharedKey" = "Symmetrischer Schlüssel";
+"tunnelPeerEndpoint" = "Endpunkt";
+"tunnelPeerPersistentKeepalive" = "Dauerhafter Keepalive";
+"tunnelPeerAllowedIPs" = "Zulässige IPs";
+"tunnelPeerRxBytes" = "Daten empfangen";
+"tunnelPeerTxBytes" = "Daten gesendet";
+"tunnelPeerLastHandshakeTime" = "Letzter Handshake";
+"tunnelPeerExcludePrivateIPs" = "Private IPs ausschließen";
+
+"tunnelSectionTitleOnDemand" = "Aktivierung auf Wunsch";
+
+"tunnelOnDemandCellular" = "Mobil";
+"tunnelOnDemandEthernet" = "LAN";
+"tunnelOnDemandWiFi" = "WLAN";
+"tunnelOnDemandSSIDsKey" = "SSIDs";
+
+"tunnelOnDemandAnySSID" = "Alle SSIDs";
+"tunnelOnDemandOnlyTheseSSIDs" = "Nur diese SSIDs";
+"tunnelOnDemandExceptTheseSSIDs" = "Ausgenommen dieser SSIDs";
+"tunnelOnDemandOnlySSID (%d)" = "Nur %d SSID";
+"tunnelOnDemandOnlySSIDs (%d)" = "Nur %d SSIDs";
+"tunnelOnDemandExceptSSID (%d)" = "Außer %d SSID";
+"tunnelOnDemandExceptSSIDs (%d)" = "Außer %d SSIDs";
+"tunnelOnDemandSSIDOptionDescriptionMac (%1$@: %2$@)" = "%1$@: %2$@";
+
+"tunnelOnDemandSSIDViewTitle" = "SSIDs";
+"tunnelOnDemandSectionTitleSelectedSSIDs" = "SSIDs";
+"tunnelOnDemandNoSSIDs" = "Keine SSIDs";
+"tunnelOnDemandSectionTitleAddSSIDs" = "SSIDs hinzufügen";
+"tunnelOnDemandAddMessageAddConnectedSSID (%@)" = "Verbundene hinzufügen: %@";
+"tunnelOnDemandAddMessageAddNewSSID" = "Neue hinzufügen";
+
+"tunnelOnDemandKey" = "On-Demand";
+"tunnelOnDemandOptionOff" = "Aus";
+"tunnelOnDemandOptionWiFiOnly" = "Nur WLAN";
+"tunnelOnDemandOptionWiFiOrCellular" = "WLAN oder Mobil";
+"tunnelOnDemandOptionCellularOnly" = "Nur Mobil";
+"tunnelOnDemandOptionWiFiOrEthernet" = "WLAN oder LAN";
+"tunnelOnDemandOptionEthernetOnly" = "Nur LAN";
+
+"addPeerButtonTitle" = "Peer hinzufügen";
+
+"deletePeerButtonTitle" = "Peer löschen";
+"deletePeerConfirmationAlertButtonTitle" = "Löschen";
+"deletePeerConfirmationAlertMessage" = "Diesen Peer löschen?";
+
+"deleteTunnelButtonTitle" = "Tunnel löschen";
+"deleteTunnelConfirmationAlertButtonTitle" = "Löschen";
+"deleteTunnelConfirmationAlertMessage" = "Diesen Tunnel löschen?";
+
+"tunnelEditPlaceholderTextRequired" = "Erforderlich";
+"tunnelEditPlaceholderTextOptional" = "Optional";
+"tunnelEditPlaceholderTextAutomatic" = "Automatisch";
+"tunnelEditPlaceholderTextStronglyRecommended" = "Stark empfohlen";
+"tunnelEditPlaceholderTextOff" = "Aus";
+
+"tunnelPeerPersistentKeepaliveValue (%@)" = "alle %@ Sekunden";
+"tunnelHandshakeTimestampNow" = "Jetzt";
+"tunnelHandshakeTimestampSystemClockBackward" = "(Systemuhr zurückgestellt)";
+"tunnelHandshakeTimestampAgo (%@)" = "vor %@";
+"tunnelHandshakeTimestampYear (%d)" = "%d Jahr";
+"tunnelHandshakeTimestampYears (%d)" = "%d Jahre";
+"tunnelHandshakeTimestampDay (%d)" = "%d Tag";
+"tunnelHandshakeTimestampDays (%d)" = "%d Tage";
+"tunnelHandshakeTimestampHour (%d)" = "%d Stunde";
+"tunnelHandshakeTimestampHours (%d)" = "%d Stunden";
+"tunnelHandshakeTimestampMinute (%d)" = "%d Minute";
+"tunnelHandshakeTimestampMinutes (%d)" = "%d Minuten";
+"tunnelHandshakeTimestampSecond (%d)" = "%d Sekunde";
+"tunnelHandshakeTimestampSeconds (%d)" = "%d Sekunden";
+
+"tunnelHandshakeTimestampHours hh:mm:ss (%@)" = "%@ Stunden";
+"tunnelHandshakeTimestampMinutes mm:ss (%@)" = "%@ Minuten";
+
+"tunnelPeerPresharedKeyEnabled" = "aktiviert";
+
+// Error alerts while creating / editing a tunnel configuration
+/* Alert title for error in the interface data */
+
+"alertInvalidInterfaceTitle" = "Ungültige Schnittstelle";
+
+/* Any one of the following alert messages can go with the above title */
+"alertInvalidInterfaceMessageNameRequired" = "Name der Schnittstelle ist erforderlich";
+"alertInvalidInterfaceMessagePrivateKeyRequired" = "Der private Interface-Schlüssel ist erforderlich";
+"alertInvalidInterfaceMessagePrivateKeyInvalid" = "Der private Schlüssel der Schnittstelle muss ein 32-Byte-Schlüssel in der base64-Kodierung sein";
+"alertInvalidInterfaceMessageAddressInvalid" = "Schnittstellen-Adressen müssen eine Liste von kommaseparierten IP-Adressen sein, optional in CIDR-Notation";
+"alertInvalidInterfaceMessageListenPortInvalid" = "Der Schnittstellen-Listen-Port muss zwischen 0 und 65535 liegen oder nicht spezifiziert sein";
+"alertInvalidInterfaceMessageMTUInvalid" = "Die MTU der Schnittstelle muss zwischen 576 und 65535 oder nicht spezifiziert sein";
+"alertInvalidInterfaceMessageDNSInvalid" = "Die DNS-Server der Schnittstelle müssen eine Liste von kommaseparierten IP-Adressen sein";
+
+/* Alert title for error in the peer data */
+"alertInvalidPeerTitle" = "Ungültiger Peer";
+
+/* Any one of the following alert messages can go with the above title */
+"alertInvalidPeerMessagePublicKeyRequired" = "Der öffentliche Schlüssel des Peers wird benötigt";
+"alertInvalidPeerMessagePublicKeyInvalid" = "Der private Schlüssel des Peers muss ein 32-Byte-Schlüssel in base64-Kodierung sein";
+"alertInvalidPeerMessagePreSharedKeyInvalid" = "Der mit dem Peer geteilte Schlüssel muss ein 32-Byte-Schlüssel in base64-Kodierung sein";
+"alertInvalidPeerMessageAllowedIPsInvalid" = "Erlaubte IPs des Teilnehmers müssen eine Liste kommaseparierter IP-Adressen sein, optional in CIDR-Notation";
+"alertInvalidPeerMessageEndpointInvalid" = "Der Endpunkt des Peers muss dem Format 'host:port' oder '[host]:port' entsprechen";
+"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "Der dauerhafte Keepalive des Peers muss zwischen 0 und 65535 oder nicht spezifiziert sein";
+"alertInvalidPeerMessagePublicKeyDuplicated" = "Zwei oder mehr Peers können nicht den gleichen öffentlichen Schlüssel haben";
+
+// Scanning QR code UI
+
+"scanQRCodeViewTitle" = "QR-Code scannen";
+"scanQRCodeTipText" = "Tipp: Mit `qrencode -t ansiutf8 < tunnel.conf` generieren";
+
+// Scanning QR code alerts
+
+"alertScanQRCodeCameraUnsupportedTitle" = "Kamera nicht unterstützt";
+"alertScanQRCodeCameraUnsupportedMessage" = "Dieses Gerät kann QR-Codes nicht scannen";
+
+"alertScanQRCodeInvalidQRCodeTitle" = "Ungültiger QR-Code";
+"alertScanQRCodeInvalidQRCodeMessage" = "Der gescannte QR-Code ist keine gültige WireGuard-Konfiguration";
+
+"alertScanQRCodeUnreadableQRCodeTitle" = "Ungültiger Code";
+"alertScanQRCodeUnreadableQRCodeMessage" = "Der gescannte Code konnte nicht gelesen werden";
+
+"alertScanQRCodeNamePromptTitle" = "Bitte den gescannten Tunnel benennen";
+
+// Settings UI
+
+"settingsViewTitle" = "Einstellungen";
+
+"settingsSectionTitleAbout" = "Über";
+"settingsVersionKeyWireGuardForIOS" = "WireGuard für iOS";
+"settingsVersionKeyWireGuardGoBackend" = "WireGuard Go Backend";
+
+"settingsSectionTitleExportConfigurations" = "Konfigurationen exportieren";
+"settingsExportZipButtonTitle" = "Zip-Archiv exportieren";
+
+"settingsSectionTitleTunnelLog" = "Log";
+"settingsViewLogButtonTitle" = "Log anzeigen";
+
+// Log view
+
+"logViewTitle" = "Log";
+
+// Log alerts
+
+"alertUnableToRemovePreviousLogTitle" = "Log-Export fehlgeschlagen";
+"alertUnableToRemovePreviousLogMessage" = "Das bereits vorhandene Log konnte nicht gelöscht werden";
+
+"alertUnableToWriteLogTitle" = "Log-Export fehlgeschlagen";
+"alertUnableToWriteLogMessage" = "Konnte Logs nicht in Datei schreiben";
+
+// Zip import / export error alerts
+
+"alertCantOpenInputZipFileTitle" = "Zip-Archiv kann nicht gelesen werden";
+"alertCantOpenInputZipFileMessage" = "Das Zip-Archiv konnte nicht gelesen werden.";
+
+"alertCantOpenOutputZipFileForWritingTitle" = "Zip-Archiv kann nicht erstellt werden";
+"alertCantOpenOutputZipFileForWritingMessage" = "Zip-Datei konnte nicht zum Schreiben geöffnet werden.";
+
+"alertBadArchiveTitle" = "Zip-Archiv kann nicht gelesen werden";
+"alertBadArchiveMessage" = "Fehlerhaftes oder beschädigtes Zip-Archiv.";
+
+"alertNoTunnelsToExportTitle" = "Nichts zum Exportieren";
+"alertNoTunnelsToExportMessage" = "Keine Tunnel zum Exportieren vorhanden";
+
+"alertNoTunnelsInImportedZipArchiveTitle" = "Keine Tunnel im Zip-Archiv";
+"alertNoTunnelsInImportedZipArchiveMessage" = "Es wurden keine .conf Tunneldateien im Zip-Archiv gefunden.";
+
+// Conf import error alerts
+
+"alertCantOpenInputConfFileTitle" = "Import aus Datei nicht möglich";
+"alertCantOpenInputConfFileMessage (%@)" = "Die Datei ‘%@’ konnte nicht gelesen werden.";
+
+// Tunnel management error alerts
+
+"alertTunnelActivationFailureTitle" = "Aktivierungsfehler";
+"alertTunnelActivationFailureMessage" = "Der Tunnel konnte nicht aktiviert werden. Bitte stelle sicher, dass du mit dem Internet verbunden bist.";
+"alertTunnelActivationSavedConfigFailureMessage" = "Tunnelinformationen konnten nicht von der gespeicherten Konfiguration abgerufen werden.";
+"alertTunnelActivationBackendFailureMessage" = "Die Go-Backend-Bibliothek kann nicht aktiviert werden.";
+"alertTunnelActivationFileDescriptorFailureMessage" = "TUN Dateideskriptor kann nicht ermittelt werden.";
+"alertTunnelActivationSetNetworkSettingsMessage" = "Netzwerkeinstellungen können nicht auf Tunnelobjekt angewendet werden.";
+
+"alertTunnelActivationFailureOnDemandAddendum" = " Dieser Tunnel hat 'Activate on Demand' aktiviert, so dass dieser Tunnel vom Betriebssystem automatisch aktiviert werden kann. Du kannst 'Activate on Demand' in dieser App deaktivieren, indem du die Tunnelkonfiguration änderst.";
+
+"alertTunnelDNSFailureTitle" = "DNS-Auflösungsfehler";
+"alertTunnelDNSFailureMessage" = "Eine oder mehrere Endpunkt-Domains konnten nicht aufgelöst werden.";
+
+"alertTunnelNameEmptyTitle" = "Kein Name angegeben";
+"alertTunnelNameEmptyMessage" = "Kann Tunnel mit leerem Namen nicht erstellen";
+
+"alertTunnelAlreadyExistsWithThatNameTitle" = "Name bereits vorhanden";
+"alertTunnelAlreadyExistsWithThatNameMessage" = "Ein Tunnel mit diesem Namen ist bereits vorhanden";
+
+"alertTunnelActivationErrorTunnelIsNotInactiveTitle" = "Aktivierung läuft";
+"alertTunnelActivationErrorTunnelIsNotInactiveMessage" = "Der Tunnel ist bereits aktiv oder wird gerade aktiviert";
+
+// Tunnel management error alerts on system error
+/* The alert message that goes with the following titles would be
+ one of the alertSystemErrorMessage* listed further down */
+
+"alertSystemErrorOnListingTunnelsTitle" = "Kann Tunnel nicht auflisten";
+"alertSystemErrorOnAddTunnelTitle" = "Kann Tunnel nicht erstellen";
+"alertSystemErrorOnModifyTunnelTitle" = "Kann Tunnel nicht ändern";
+"alertSystemErrorOnRemoveTunnelTitle" = "Kann Tunnel nicht löschen";
+
+/* The alert message for this alert shall include
+ one of the alertSystemErrorMessage* listed further down */
+"alertTunnelActivationSystemErrorTitle" = "Aktivierungsfehler";
+"alertTunnelActivationSystemErrorMessage (%@)" = "Der Tunnel konnte nicht aktiviert werden. %@";
+
+/* alertSystemErrorMessage* messages */
+"alertSystemErrorMessageTunnelConfigurationInvalid" = "Die Konfiguration ist ungültig.";
+"alertSystemErrorMessageTunnelConfigurationDisabled" = "Die Konfiguration ist deaktiviert.";
+"alertSystemErrorMessageTunnelConnectionFailed" = "Verbindung fehlgeschlagen.";
+"alertSystemErrorMessageTunnelConfigurationStale" = "Die Konfiguration ist veraltet.";
+"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "Lesen oder speichern der Konfiguration gescheitert.";
+"alertSystemErrorMessageTunnelConfigurationUnknown" = "Unbekannter Systemfehler.";
+
+// Mac status bar menu / pulldown menu / main menu
+
+"macMenuNetworks (%@)" = "Netzwerke:%@";
+"macMenuNetworksNone" = "Keine Netzwerke.";
+
+"macMenuTitle" = "WireGuard";
+"macMenuManageTunnels" = "Tunnel verwalten";
+"macMenuImportTunnels" = "Tunnel aus Datei importieren…";
+"macMenuAddEmptyTunnel" = "Leeren Tunnel hinzufügen…";
+"macMenuViewLog" = "Protokoll anzeigen";
+"macMenuExportTunnels" = "Tunnel als Zip exportieren…";
+"macMenuAbout" = "Über WireGuard";
+"macMenuQuit" = "WireGuard beenden";
+
+"macMenuHideApp" = "WireGuard ausblenden";
+"macMenuHideOtherApps" = "Andere ausblenden";
+"macMenuShowAllApps" = "Alle anzeigen";
+
+"macMenuFile" = "Datei";
+"macMenuCloseWindow" = "Fenster schließen";
+
+"macMenuEdit" = "Bearbeiten";
+"macMenuCut" = "Ausschneiden";
+"macMenuCopy" = "Kopieren";
+"macMenuPaste" = "Einfügen";
+"macMenuSelectAll" = "Alle markieren";
+
+"macMenuTunnel" = "Tunnel";
+"macMenuToggleStatus" = "Status umschalten";
+"macMenuEditTunnel" = "Bearbeiten…";
+"macMenuDeleteSelected" = "Ausgewählte löschen";
+
+"macMenuWindow" = "Fenster";
+"macMenuMinimize" = "Minimieren";
+"macMenuZoom" = "Vergrößern";
+
+// Mac manage tunnels window
+
+"macWindowTitleManageTunnels" = "WireGuard-Tunnel verwalten";
+
+"macDeleteTunnelConfirmationAlertMessage (%@)" = "Möchten Sie ’%@’ wirklich löschen?";
+"macDeleteMultipleTunnelsConfirmationAlertMessage (%d)" = "Möchten Sie diese %d Tunnel wirklich löschen?";
+"macDeleteTunnelConfirmationAlertInfo" = "Diese Aktion kann nicht rückgängig gemacht werden.";
+"macDeleteTunnelConfirmationAlertButtonTitleDelete" = "Löschen";
+"macDeleteTunnelConfirmationAlertButtonTitleCancel" = "Abbrechen";
+"macDeleteTunnelConfirmationAlertButtonTitleDeleting" = "Lösche…";
+
+"macButtonImportTunnels" = "Tunnel aus Datei importieren";
+"macSheetButtonImport" = "Importieren";
+
+"macNameFieldExportLog" = "Log speichern unter:";
+"macSheetButtonExportLog" = "Speichern";
+
+"macNameFieldExportZip" = "Tunnel exportieren nach:";
+"macSheetButtonExportZip" = "Speichern";
+
+"macButtonDeleteTunnels (%d)" = "%d Tunnel löschen";
+
+"macButtonEdit" = "Bearbeiten";
+
+// Mac detail/edit view fields
+
+"macFieldKey (%@)" = "%@:";
+"macFieldOnDemand" = "On-Demand:";
+"macFieldOnDemandSSIDs" = "SSIDs:";
+
+// Mac status display
+
+"macStatus (%@)" = "Status: %@";
+
+// Mac editing config
+
+"macEditDiscard" = "Verwerfen";
+"macEditSave" = "Speichern";
+
+"macAlertNameIsEmpty" = "Ein Name ist erforderlich";
+"macAlertDuplicateName (%@)" = "Ein anderer Tunnel mit dem Namen ‘%@ ’ existiert bereits.";
+
+"macAlertInvalidLine (%@)" = "Ungültige Zeile: ‘%@’.";
+
+"macAlertNoInterface" = "Die Konfiguration muss einen Abschnitt „Interface“ aufweisen.";
+"macAlertMultipleInterfaces" = "Die Konfiguration darf nur einen Abschnitt „Interface“ aufweisen.";
+"macAlertPrivateKeyInvalid" = "Der private Schlüssel ist ungültig.";
+"macAlertListenPortInvalid (%@)" = "Eingangs-Port '%@' ungültig.";
+"macAlertAddressInvalid (%@)" = "Adresse ‘%@’ ungültig.";
+"macAlertDNSInvalid (%@)" = "DNS ‘%@’ ungültig.";
+"macAlertMTUInvalid (%@)" = "MTU ‘%@’ ungültig.";
+
+"macAlertUnrecognizedInterfaceKey (%@)" = "Interface enthält nicht erkannten Schlüssel ‘%@’";
+"macAlertInfoUnrecognizedInterfaceKey" = "Gültige Schlüssel sind: 'PrivateKey', 'ListenPort', 'Address', 'DNS' und 'MTU'.";
+
+"macAlertPublicKeyInvalid" = "Der öffentliche Schlüssel ist ungültig";
+"macAlertPreSharedKeyInvalid" = "Der vorab geteilte Schlüssel ist ungültig";
+"macAlertAllowedIPInvalid (%@)" = "Erlaubte IP ‘%@’ ist ungültig";
+"macAlertEndpointInvalid (%@)" = "Endpunkt ‘%@’ ist ungültig";
+"macAlertPersistentKeepliveInvalid (%@)" = "Dauerhafter Keepalive ‘%@’ ist ungültig";
+
+"macAlertUnrecognizedPeerKey (%@)" = "Teilnehmer enthält nicht erkannten Schlüssel ‘%@’";
+"macAlertInfoUnrecognizedPeerKey" = "Gültige Schlüssel sind: 'PublicKey', 'PresharedKey', 'AllowedIPs', 'Endpoint' und 'PersistentKeepalive'";
+
+"macAlertMultipleEntriesForKey (%@)" = "Es darf nur einen Eintrag pro Abschnitt für den Schlüssel ‘%@ ’ geben";
+
+// Mac about dialog
+
+"macAppVersion (%@)" = "App-Version: %@";
+"macGoBackendVersion (%@)" = "Go-Backend Version: %@";
+
+// Privacy
+
+"macExportPrivateData" = "exportiere private Schlüssel für Tunnel";
+"macViewPrivateData" = "zeige private Schlüssel für Tunnel";
+"iosExportPrivateData" = "Authentifizieren, um private Schlüssel für Tunnel zu exportieren.";
+"iosViewPrivateData" = "Authentifizieren, um private Schlüssel für Tunnel anzuzeigen.";
+
+// Mac alert
+
+"macConfirmAndQuitAlertMessage" = "Möchten Sie den Tunnelmanager schließen oder WireGuard ganz beenden?";
+"macConfirmAndQuitAlertInfo" = "Wenn Sie den Tunnelmanager schließen, wird WireGuard weiterhin über das Symbol in der Menüleiste zur Verfügung stehen.";
+"macConfirmAndQuitInfoWithActiveTunnel (%@)" = "Wenn Sie den Tunnelmanager schließen, wird WireGuard weiterhin über das Symbol in der Menüleiste zur Verfügung stehen.\n\nBeachten Sie, dass der derzeit aktive Tunnel ('%@') weiterhin aktiv bleibt (auch wenn Sie WireGuard ganz beenden), bis Sie ihn von dieser Anwendung oder über das Netzwerk-Panel in den Systemeinstellungen deaktivieren.";
+"macConfirmAndQuitAlertQuitWireGuard" = "WireGuard beenden";
+"macConfirmAndQuitAlertCloseWindow" = "Tunnelverwaltung schließen";
+
+"macAppExitingWithActiveTunnelMessage" = "WireGuard wird beendet, mit einem aktiven Tunnel";
+"macAppExitingWithActiveTunnelInfo" = "Der Tunnel bleibt nach dem Beenden aktiv. Sie können ihn deaktivieren, indem Sie diese Anwendung erneut öffnen oder in den Systemeinstellungen unter \"Netzwerk\" aktivieren.";
+
+// Mac tooltip
+
+"macToolTipEditTunnel" = "Tunnel bearbeiten (⌘E)";
+"macToolTipToggleStatus" = "Status umschalten (⌘T)";
+
+// Mac log view
+
+"macLogColumnTitleTime" = "Zeit";
+"macLogColumnTitleLogMessage" = "Protokoll-Nachricht";
+"macLogButtonTitleClose" = "Schließen";
+"macLogButtonTitleSave" = "Speichern…";
+
+// Mac unusable tunnel view
+
+"macUnusableTunnelMessage" = "Die Konfiguration für diesen Tunnel kann nicht am Schlüsselbund gefunden werden.";
+"macUnusableTunnelInfo" = "Falls dieser Tunnel von einem anderen Benutzer erstellt wurde, kann nur dieser den Tunnel anzeigen, bearbeiten oder aktivieren.";
+"macUnusableTunnelButtonTitleDeleteTunnel" = "Tunnel löschen";
+
+// Mac App Store updating alert
+
+"macAppStoreUpdatingAlertMessage" = "App Store möchte WireGuard aktualisieren";
+"macAppStoreUpdatingAlertInfoWithOnDemand (%@)" = "Bitte deaktivieren Sie \"on-demand\" für den Tunnel ‘%@', deaktivieren Sie ihn und fahren Sie dann mit der Aktualisierung im App Store fort.";
+"macAppStoreUpdatingAlertInfoWithoutOnDemand (%@)" = "Bitte deaktivieren Sie den Tunnel ‘%@’ und fahren Sie dann mit der Aktualisierung im App Store fort.";
+
+// Donation
+
+"donateLink" = "♥ Spende an das WireGuard Projekt";
diff --git a/Sources/WireGuardApp/it.lproj/Localizable.strings b/Sources/WireGuardApp/it.lproj/Localizable.strings
new file mode 100644
index 0000000..d90745a
--- /dev/null
+++ b/Sources/WireGuardApp/it.lproj/Localizable.strings
@@ -0,0 +1,445 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
+
+// Generic alert action names
+
+"actionOK" = "OK";
+"actionCancel" = "Annulla";
+"actionSave" = "Salva";
+
+// Tunnels list UI
+
+"tunnelsListTitle" = "WireGuard";
+"tunnelsListSettingsButtonTitle" = "Impostazioni";
+"tunnelsListCenteredAddTunnelButtonTitle" = "Aggiungi un tunnel";
+"tunnelsListSwipeDeleteButtonTitle" = "Elimina";
+"tunnelsListSelectButtonTitle" = "Seleziona";
+"tunnelsListSelectAllButtonTitle" = "Seleziona tutto";
+"tunnelsListDeleteButtonTitle" = "Elimina";
+"tunnelsListSelectedTitle (%d)" = "%d selezionati";
+
+// Tunnels list menu
+
+"addTunnelMenuHeader" = "Aggiungi un nuovo tunnel WireGuard";
+"addTunnelMenuImportFile" = "Crea da file o archivio";
+"addTunnelMenuQRCode" = "Crea da codice QR";
+"addTunnelMenuFromScratch" = "Crea da zero";
+
+// Tunnels list alerts
+
+"alertImportedFromMultipleFilesTitle (%d)" = "Creati %d tunnel";
+"alertImportedFromMultipleFilesMessage (%1$d of %2$d)" = "Creati %1$d di %2$d tunnel da file importati";
+
+"alertImportedFromZipTitle (%d)" = "Creati %d tunnel";
+"alertImportedFromZipMessage (%1$d of %2$d)" = "Creati %1$d di %2$d tunnel da file importati";
+
+"alertBadConfigImportTitle" = "Impossibile importare il tunnel";
+"alertBadConfigImportMessage (%@)" = "Il file “%@” non contiene una configurazione di WireGuard valida";
+
+"deleteTunnelsConfirmationAlertButtonTitle" = "Elimina";
+"deleteTunnelConfirmationAlertButtonMessage (%d)" = "Eliminare %d tunnel?";
+"deleteTunnelsConfirmationAlertButtonMessage (%d)" = "Eliminare %d tunnel?";
+
+// Tunnel detail and edit UI
+
+"newTunnelViewTitle" = "Nuova configurazione";
+"editTunnelViewTitle" = "Modifica configurazione";
+
+"tunnelSectionTitleStatus" = "Stato";
+
+"tunnelStatusInactive" = "Inattivo";
+"tunnelStatusActivating" = "Attivazione";
+"tunnelStatusActive" = "Attivo";
+"tunnelStatusDeactivating" = "Disattivazione";
+"tunnelStatusReasserting" = "Riattivazione";
+"tunnelStatusRestarting" = "Riavvio";
+"tunnelStatusWaiting" = "In attesa";
+
+"macToggleStatusButtonActivate" = "Attivato";
+"macToggleStatusButtonActivating" = "Attivazione…";
+"macToggleStatusButtonDeactivate" = "Disattiva";
+"macToggleStatusButtonDeactivating" = "Disattivazione…";
+"macToggleStatusButtonReasserting" = "Riattivazione…";
+"macToggleStatusButtonRestarting" = "Riavvio…";
+"macToggleStatusButtonWaiting" = "In attesa…";
+
+"tunnelSectionTitleInterface" = "Interfaccia";
+
+"tunnelInterfaceName" = "Nome";
+"tunnelInterfacePrivateKey" = "Chiave privata";
+"tunnelInterfacePublicKey" = "Chiave pubblica";
+"tunnelInterfaceGenerateKeypair" = "Genera coppia di chiavi";
+"tunnelInterfaceAddresses" = "Indirizzi";
+"tunnelInterfaceListenPort" = "Porta in ascolto";
+"tunnelInterfaceMTU" = "MTU";
+"tunnelInterfaceDNS" = "Server DNS";
+"tunnelInterfaceStatus" = "Stato";
+
+"tunnelSectionTitlePeer" = "Peer";
+
+"tunnelPeerPublicKey" = "Chiave pubblica";
+"tunnelPeerPreSharedKey" = "Chiave condivisa";
+"tunnelPeerEndpoint" = "Endpoint";
+"tunnelPeerPersistentKeepalive" = "Tieni sempre attivo";
+"tunnelPeerAllowedIPs" = "IP consentiti";
+"tunnelPeerRxBytes" = "Dati ricevuti";
+"tunnelPeerTxBytes" = "Dati inviati";
+"tunnelPeerLastHandshakeTime" = "Ultimo handshake";
+"tunnelPeerExcludePrivateIPs" = "Escludi IP privati";
+
+"tunnelSectionTitleOnDemand" = "Attivazione su richiesta";
+
+"tunnelOnDemandCellular" = "Cellulare";
+"tunnelOnDemandEthernet" = "Ethernet";
+"tunnelOnDemandWiFi" = "Wi-Fi";
+"tunnelOnDemandSSIDsKey" = "SSID";
+
+"tunnelOnDemandAnySSID" = "Qualsiasi SSID";
+"tunnelOnDemandOnlyTheseSSIDs" = "Solo questi SSID";
+"tunnelOnDemandExceptTheseSSIDs" = "Tranne questi SSID";
+"tunnelOnDemandOnlySSID (%d)" = "Solo %d SSID";
+"tunnelOnDemandOnlySSIDs (%d)" = "Solo %d SSID";
+"tunnelOnDemandExceptSSID (%d)" = "Tranne %d SSID";
+"tunnelOnDemandExceptSSIDs (%d)" = "Tranne %d SSID";
+"tunnelOnDemandSSIDOptionDescriptionMac (%1$@: %2$@)" = "%1$@: %2$@";
+
+"tunnelOnDemandSSIDViewTitle" = "SSIDs";
+"tunnelOnDemandSectionTitleSelectedSSIDs" = "SSIDs";
+"tunnelOnDemandNoSSIDs" = "Nessun SSIDs";
+"tunnelOnDemandSectionTitleAddSSIDs" = "Aggiungi SSIDs";
+"tunnelOnDemandAddMessageAddConnectedSSID (%@)" = "Aggiungi connessione %@";
+"tunnelOnDemandAddMessageAddNewSSID" = "Aggiungi nuovo";
+
+"tunnelOnDemandKey" = "Su richiesta";
+"tunnelOnDemandOptionOff" = "Off";
+"tunnelOnDemandOptionWiFiOnly" = "Solo Wi-1Fi";
+"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi o dati";
+"tunnelOnDemandOptionCellularOnly" = "Solo dati";
+"tunnelOnDemandOptionWiFiOrEthernet" = "Wi-Fi o ethernet";
+"tunnelOnDemandOptionEthernetOnly" = "Solo ethernet";
+
+"addPeerButtonTitle" = "Aggiungi peer";
+
+"deletePeerButtonTitle" = "Elimina peer";
+"deletePeerConfirmationAlertButtonTitle" = "Elimina";
+"deletePeerConfirmationAlertMessage" = "Eliminare questo peer?";
+
+"deleteTunnelButtonTitle" = "Elimina tunnel";
+"deleteTunnelConfirmationAlertButtonTitle" = "Elimina";
+"deleteTunnelConfirmationAlertMessage" = "Vuoi eliminare questo tunnel?";
+
+"tunnelEditPlaceholderTextRequired" = "Richiesto";
+"tunnelEditPlaceholderTextOptional" = "Facoltativo";
+"tunnelEditPlaceholderTextAutomatic" = "Automatico";
+"tunnelEditPlaceholderTextStronglyRecommended" = "Fortemente consigliato";
+"tunnelEditPlaceholderTextOff" = "Off";
+
+"tunnelPeerPersistentKeepaliveValue (%@)" = "ogni %@ secondi";
+"tunnelHandshakeTimestampNow" = "Ora";
+"tunnelHandshakeTimestampSystemClockBackward" = "(L'orologio di sistema va all'indietro)";
+"tunnelHandshakeTimestampAgo (%@)" = "%@ fa";
+"tunnelHandshakeTimestampYear (%d)" = "%d anno";
+"tunnelHandshakeTimestampYears (%d)" = "%d anni";
+"tunnelHandshakeTimestampDay (%d)" = "%d giorno";
+"tunnelHandshakeTimestampDays (%d)" = "%d giorni";
+"tunnelHandshakeTimestampHour (%d)" = "%d ora";
+"tunnelHandshakeTimestampHours (%d)" = "%d ore";
+"tunnelHandshakeTimestampMinute (%d)" = "%d minuto";
+"tunnelHandshakeTimestampMinutes (%d)" = "%d minuti";
+"tunnelHandshakeTimestampSecond (%d)" = "%d secondo";
+"tunnelHandshakeTimestampSeconds (%d)" = "%d secondi";
+
+"tunnelHandshakeTimestampHours hh:mm:ss (%@)" = "%@ ore";
+"tunnelHandshakeTimestampMinutes mm:ss (%@)" = "%@ minuti";
+
+"tunnelPeerPresharedKeyEnabled" = "abilitata";
+
+// Error alerts while creating / editing a tunnel configuration
+/* Alert title for error in the interface data */
+
+"alertInvalidInterfaceTitle" = "Interfaccia non valida";
+
+/* Any one of the following alert messages can go with the above title */
+"alertInvalidInterfaceMessageNameRequired" = "Il nome dell'interfaccia è richiesto";
+"alertInvalidInterfaceMessagePrivateKeyRequired" = "La chiave privata dell'interfaccia è necessaria";
+"alertInvalidInterfaceMessagePrivateKeyInvalid" = "La chiave privata dell'interfaccia deve essere una chiave a 32 byte con codifica base64";
+"alertInvalidInterfaceMessageAddressInvalid" = "Gli indirizzi dell'interfaccia devono essere una lista di indirizzi IP separati da virgole, facoltativamente nella notazione CIDR";
+"alertInvalidInterfaceMessageListenPortInvalid" = "La porta di ascolto dell'interfaccia deve essere compresa tra 0 e 65535, o non specificata";
+"alertInvalidInterfaceMessageMTUInvalid" = "Il valore di MTU dell'interfaccia deve essere compreso tra 576 e 65535, o non specificato";
+"alertInvalidInterfaceMessageDNSInvalid" = "I server DNS dell'interfaccia devono essere una elenco di indirizzi IP separati da virgole";
+
+/* Alert title for error in the peer data */
+"alertInvalidPeerTitle" = "Peer non valido";
+
+/* Any one of the following alert messages can go with the above title */
+"alertInvalidPeerMessagePublicKeyRequired" = "La chiave pubblica del peer è richiesta";
+"alertInvalidPeerMessagePublicKeyInvalid" = "La chiave pubblica del peer deve essere una chiave a 32 byte con codifica base64";
+"alertInvalidPeerMessagePreSharedKeyInvalid" = "La chiave pre-condivisa del peer deve essere una chiave a 32 byte con codifica base64";
+"alertInvalidPeerMessageAllowedIPsInvalid" = "Gli IP consentiti dal peer devono essere un elenco di indirizzi IP separati da virgole, facoltativamente nella notazione CIDR";
+"alertInvalidPeerMessageEndpointInvalid" = "L'endpoint del peer deve essere nella forma 'host:porta' o '[host]:porta'";
+"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "Il keepalive permanente del peer deve essere compreso tra 0 e 65535, o non specificato";
+"alertInvalidPeerMessagePublicKeyDuplicated" = "Due o più peer non possono avere la stessa chiave pubblica";
+
+// Scanning QR code UI
+
+"scanQRCodeViewTitle" = "Scansione codice QR";
+"scanQRCodeTipText" = "Suggerimento: generalo con `qrencode -t ansiutf8 < tunnel.conf`";
+
+// Scanning QR code alerts
+
+"alertScanQRCodeCameraUnsupportedTitle" = "Fotocamera non supportata";
+"alertScanQRCodeCameraUnsupportedMessage" = "Questo dispositivo non è in grado di analizzare codici QR";
+
+"alertScanQRCodeInvalidQRCodeTitle" = "Codice QR non valido";
+"alertScanQRCodeInvalidQRCodeMessage" = "Il codice QR analizzato non è una configurazione valida di WireGuard";
+
+"alertScanQRCodeUnreadableQRCodeTitle" = "Codice non valido";
+"alertScanQRCodeUnreadableQRCodeMessage" = "Il codice analizzato non può essere letto";
+
+"alertScanQRCodeNamePromptTitle" = "Assegna un nome al tunnel analizzato";
+
+// Settings UI
+
+"settingsViewTitle" = "Impostazioni";
+
+"settingsSectionTitleAbout" = "Informazioni";
+"settingsVersionKeyWireGuardForIOS" = "WireGuard per iOS";
+"settingsVersionKeyWireGuardGoBackend" = "Backend Go di WireGuard";
+
+"settingsSectionTitleExportConfigurations" = "Esporta configurazioni";
+"settingsExportZipButtonTitle" = "Esporta archivio zip";
+
+"settingsSectionTitleTunnelLog" = "Log";
+"settingsViewLogButtonTitle" = "Visualizza log";
+
+// Log view
+
+"logViewTitle" = "Log";
+
+// Log alerts
+
+"alertUnableToRemovePreviousLogTitle" = "Esportazione log non riuscita";
+"alertUnableToRemovePreviousLogMessage" = "Il log pre-esistente non può essere svuotato";
+
+"alertUnableToWriteLogTitle" = "Esportazione log non riuscita";
+"alertUnableToWriteLogMessage" = "Impossibile scrivere i log su file";
+
+// Zip import / export error alerts
+
+"alertCantOpenInputZipFileTitle" = "Impossibile leggere l'archivio zip";
+"alertCantOpenInputZipFileMessage" = "L'archivio zip non può essere letto.";
+
+"alertCantOpenOutputZipFileForWritingTitle" = "Impossibile creare l'archivio zip";
+"alertCantOpenOutputZipFileForWritingMessage" = "Impossibile aprire in file zip per la scrittura.";
+
+"alertBadArchiveTitle" = "Impossibile leggere l'archivio zip";
+"alertBadArchiveMessage" = "Archivio zip non valido o danneggiato.";
+
+"alertNoTunnelsToExportTitle" = "Niente da esportare";
+"alertNoTunnelsToExportMessage" = "Non ci sono tunnel da esportare";
+
+"alertNoTunnelsInImportedZipArchiveTitle" = "Nessun tunnel nell'archivio zip";
+"alertNoTunnelsInImportedZipArchiveMessage" = "Non è stato trovato alcun file .conf di tunnel all'interno dell'archivio zip.";
+
+// Conf import error alerts
+
+"alertCantOpenInputConfFileTitle" = "Impossibile importare da file";
+"alertCantOpenInputConfFileMessage (%@)" = "Il file ‘%@’ non può essere letto.";
+
+// Tunnel management error alerts
+
+"alertTunnelActivationFailureTitle" = "Attivazione non riuscita";
+"alertTunnelActivationFailureMessage" = "Il tunnel non può essere attivato. Assicurati di essere connesso a Internet.";
+"alertTunnelActivationSavedConfigFailureMessage" = "Impossibile recuperare le Informazioni del tunnel dalla configurazione salvata.";
+"alertTunnelActivationBackendFailureMessage" = "Impossibile attivare la libreria del backend Go.";
+"alertTunnelActivationFileDescriptorFailureMessage" = "Impossibile determinare il descrittore file del dispositivo TUN.";
+"alertTunnelActivationSetNetworkSettingsMessage" = "Impossibile applicare le Impostazioni di rete all'oggetto del tunnel.";
+
+"alertTunnelActivationFailureOnDemandAddendum" = " Questo tunnel è stato attivato su richiesta, per cui potrebbe essere riattivato automaticamente dal sistema operativo. Puoi disabilitare Attiva su richiesta in questa applicazione modificando la configurazione del tunnel.";
+
+"alertTunnelDNSFailureTitle" = "Risoluzione DNS non riuscita";
+"alertTunnelDNSFailureMessage" = "Uno o più domini di endpoint non può essere risolto.";
+
+"alertTunnelNameEmptyTitle" = "Nessun nome fornito";
+"alertTunnelNameEmptyMessage" = "Impossibile creare un tunnel con un nome vuoto";
+
+"alertTunnelAlreadyExistsWithThatNameTitle" = "Il nome esiste già";
+"alertTunnelAlreadyExistsWithThatNameMessage" = "Un tunnel con quel nome esiste già";
+
+"alertTunnelActivationErrorTunnelIsNotInactiveTitle" = "Attivazione in corso";
+"alertTunnelActivationErrorTunnelIsNotInactiveMessage" = "Il tunnel è già attivo o in fase di attivazione";
+
+// Tunnel management error alerts on system error
+/* The alert message that goes with the following titles would be
+ one of the alertSystemErrorMessage* listed further down */
+
+"alertSystemErrorOnListingTunnelsTitle" = "Impossibile elencare i tunnel";
+"alertSystemErrorOnAddTunnelTitle" = "Impossibile creare il tunnel";
+"alertSystemErrorOnModifyTunnelTitle" = "Impossibile modificare il tunnel";
+"alertSystemErrorOnRemoveTunnelTitle" = "Impossibile rimuovere il tunnel";
+
+/* The alert message for this alert shall include
+ one of the alertSystemErrorMessage* listed further down */
+"alertTunnelActivationSystemErrorTitle" = "Attivazione non riuscita";
+"alertTunnelActivationSystemErrorMessage (%@)" = "Il tunnel non può essere attivato. %@";
+
+/* alertSystemErrorMessage* messages */
+"alertSystemErrorMessageTunnelConfigurationInvalid" = "La configurazione non è valida.";
+"alertSystemErrorMessageTunnelConfigurationDisabled" = "La configurazione è disabilitata.";
+"alertSystemErrorMessageTunnelConnectionFailed" = "La connessione non è riuscita.";
+"alertSystemErrorMessageTunnelConfigurationStale" = "La configurazione è obsoleta.";
+"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "La lettura o la scrittura della configurazione non è riuscita.";
+"alertSystemErrorMessageTunnelConfigurationUnknown" = "Errore di sistema sconosciuto.";
+
+// Mac status bar menu / pulldown menu / main menu
+
+"macMenuNetworks (%@)" = "Reti: %@";
+"macMenuNetworksNone" = "Reti: nessuna";
+
+"macMenuTitle" = "WireGuard";
+"macMenuManageTunnels" = "Gestisci i tunnel";
+"macMenuImportTunnels" = "Importa tunnel da file…";
+"macMenuAddEmptyTunnel" = "Aggiungi tunnel vuoto…";
+"macMenuViewLog" = "Visualizza log";
+"macMenuExportTunnels" = "Esporta tunnel in Zip…";
+"macMenuAbout" = "Informazioni su WireGuard";
+"macMenuQuit" = "Chiudi WireGuard";
+
+"macMenuHideApp" = "Nascondi WireGuard";
+"macMenuHideOtherApps" = "Nascondi altri";
+"macMenuShowAllApps" = "Mostra tutto";
+
+"macMenuFile" = "File";
+"macMenuCloseWindow" = "Chiudi finestra";
+
+"macMenuEdit" = "Modifica";
+"macMenuCut" = "Taglia";
+"macMenuCopy" = "Copia";
+"macMenuPaste" = "Incolla";
+"macMenuSelectAll" = "Seleziona tutto";
+
+"macMenuTunnel" = "Tunnel";
+"macMenuToggleStatus" = "Stato di attivazione";
+"macMenuEditTunnel" = "Modifica…";
+"macMenuDeleteSelected" = "Elimina selezionati";
+
+"macMenuWindow" = "Finestra";
+"macMenuMinimize" = "Minimizza";
+"macMenuZoom" = "Zoom";
+
+// Mac manage tunnels window
+
+"macWindowTitleManageTunnels" = "Gestisci i tunnel di WireGuard";
+
+"macDeleteTunnelConfirmationAlertMessage (%@)" = "Sei sicuro di voler eliminare ‘%@’?";
+"macDeleteMultipleTunnelsConfirmationAlertMessage (%d)" = "Sei sicuro di voler eliminare %d tunnel?";
+"macDeleteTunnelConfirmationAlertInfo" = "Non puoi annullare questa azione.";
+"macDeleteTunnelConfirmationAlertButtonTitleDelete" = "Elimina";
+"macDeleteTunnelConfirmationAlertButtonTitleCancel" = "Annulla";
+"macDeleteTunnelConfirmationAlertButtonTitleDeleting" = "Eliminazione…";
+
+"macButtonImportTunnels" = "Importa tunnel da file";
+"macSheetButtonImport" = "Importa";
+
+"macNameFieldExportLog" = "Salva log in:";
+"macSheetButtonExportLog" = "Salva";
+
+"macNameFieldExportZip" = "Esporta i tunnel in:";
+"macSheetButtonExportZip" = "Salva";
+
+"macButtonDeleteTunnels (%d)" = "Elimina %d tunnel";
+
+"macButtonEdit" = "Modifica";
+
+// Mac detail/edit view fields
+
+"macFieldKey (%@)" = "%@:";
+"macFieldOnDemand" = "Su richiesta:";
+"macFieldOnDemandSSIDs" = "SSID:";
+
+// Mac status display
+
+"macStatus (%@)" = "Stato: %@";
+
+// Mac editing config
+
+"macEditDiscard" = "Scarta";
+"macEditSave" = "Salva";
+
+"macAlertNameIsEmpty" = "Il nome è richiesto";
+"macAlertDuplicateName (%@)" = "Un altro tunnel con il nome ‘%@’ esiste già.";
+
+"macAlertInvalidLine (%@)" = "Riga non valida: ‘%@’.";
+
+"macAlertNoInterface" = "La configurazione deve avere una sezione ‘Interface’.";
+"macAlertMultipleInterfaces" = "La configurazione deve avere solo una sezione ‘Interface’.";
+"macAlertPrivateKeyInvalid" = "La chiave privata non è valida.";
+"macAlertListenPortInvalid (%@)" = "La porta in ascolto ‘%@’ non è valida.";
+"macAlertAddressInvalid (%@)" = "L'indirizzo %@’ non è valido.";
+"macAlertDNSInvalid (%@)" = "Il DNS ‘%@’ non è valido.";
+"macAlertMTUInvalid (%@)" = "Il valore di MTU ‘%@’ non è valido.";
+
+"macAlertUnrecognizedInterfaceKey (%@)" = "L'interfaccia contiene una chiave non riconosciuta ‘%@’";
+"macAlertInfoUnrecognizedInterfaceKey" = "Chiavi valide sono: ‘PrivateKey’, ‘ListenPort’, ‘Address’, ‘DNS’ e ‘MTU’.";
+
+"macAlertPublicKeyInvalid" = "La chiave pubblica non è valida";
+"macAlertPreSharedKeyInvalid" = "La chiave pre-condivisa non è valida";
+"macAlertAllowedIPInvalid (%@)" = "L'IP consentito ‘%@’ non è valido";
+"macAlertEndpointInvalid (%@)" = "L'endpoint ‘%@’ non è valido";
+"macAlertPersistentKeepliveInvalid (%@)" = "Il valore di keepalive permanente ‘%@’ non è valido";
+
+"macAlertUnrecognizedPeerKey (%@)" = "Peer contiene una chiave non riconosciuta ‘%@’";
+"macAlertInfoUnrecognizedPeerKey" = "Chiavi valide sono: ‘PublicKey’, ‘PresharedKey’, ‘AllowedIPs’, ‘Endpoint’ e ‘PersistentKeepalive’";
+
+"macAlertMultipleEntriesForKey (%@)" = "Dovrebbe esistere solo una voce per sezione della chiave ‘%@’";
+
+// Mac about dialog
+
+"macAppVersion (%@)" = "Versione applicazione: %@";
+"macGoBackendVersion (%@)" = "Versione backend Go: %@";
+
+// Privacy
+
+"macExportPrivateData" = "esporta chiavi private dei tunnel";
+"macViewPrivateData" = "visualizza chiavi private dei tunnel";
+"iosExportPrivateData" = "Autenticati per esportare le chiavi private dei tunnel.";
+"iosViewPrivateData" = "Autenticati per visualizzare le chiavi private dei tunnel.";
+
+// Mac alert
+
+"macConfirmAndQuitAlertMessage" = "Vuoi chiudere il gestore dei tunnel o chiudere completamente WireGuard?";
+"macConfirmAndQuitAlertInfo" = "Se chiudi il gestore dei tunnel, WireGuard continuerà a essere disponibile dall'icona della barra dei menu.";
+"macConfirmAndQuitInfoWithActiveTunnel (%@)" = "Se chiudi il gestore dei tunnel, WireGuard continuerà a essere disponibile dall'icona della barra dei menu.\n\nNota che se vuoi chiudere completamente WireGuard il tunnel attualmente attivo ('%@') rimarrà attivo fino a quando lo disabiliti da questa applicazione o tramite il pannello Rete nelle Preferenze di Sistema.";
+"macConfirmAndQuitAlertQuitWireGuard" = "Chiudi WireGuard";
+"macConfirmAndQuitAlertCloseWindow" = "Chiudi il gestore dei tunnel";
+
+"macAppExitingWithActiveTunnelMessage" = "WireGuard sta per essere chiuso con un tunnel attivo";
+"macAppExitingWithActiveTunnelInfo" = "Il tunnel rimarrà attivo dopo l'uscita. Puoi disabilitarlo riaprendo questa applicazione o tramite il pannello Rete nelle Preferenze di Sistema.";
+
+// Mac tooltip
+
+"macToolTipEditTunnel" = "Modifica tunnel (⌘E)";
+"macToolTipToggleStatus" = "Commuta stato (⌘T)";
+
+// Mac log view
+
+"macLogColumnTitleTime" = "Tempo";
+"macLogColumnTitleLogMessage" = "Messaggio di log";
+"macLogButtonTitleClose" = "Chiudi";
+"macLogButtonTitleSave" = "Salva…";
+
+// Mac unusable tunnel view
+
+"macUnusableTunnelMessage" = "La configurazione per questo tunnel non è presente nel tuo portachiavi.";
+"macUnusableTunnelInfo" = "Nel caso in cui questo tunnel sia stato creato da un altro utente, solo tale utente può visualizzare, modificare o attivare il tunnel.";
+"macUnusableTunnelButtonTitleDeleteTunnel" = "Elimina tunnel";
+
+// Mac App Store updating alert
+
+"macAppStoreUpdatingAlertMessage" = "App Store vuole aggiornare WireGuard";
+"macAppStoreUpdatingAlertInfoWithOnDemand (%@)" = "Disabilita l'attivazione su richiesta del tunnel ‘%@’, disattivalo e continua l'aggiornamento in App Store.";
+"macAppStoreUpdatingAlertInfoWithoutOnDemand (%@)" = "Disattiva il tunnel ‘%@’ e continua l'aggiornamento in App Store.";
+
+// Donation
+
+"donateLink" = "♥ Fai una donazione al progetto WireGuard";
diff --git a/Sources/WireGuardApp/ja.lproj/Localizable.strings b/Sources/WireGuardApp/ja.lproj/Localizable.strings
new file mode 100644
index 0000000..66de5b7
--- /dev/null
+++ b/Sources/WireGuardApp/ja.lproj/Localizable.strings
@@ -0,0 +1,445 @@
+// SPDX-License-Identifier: MIT
+// Copyright © 2018-2020 WireGuard LLC. All Rights Reserved.
+
+// Generic alert action names
+
+"actionOK" = "OK";
+"actionCancel" = "キャンセル";
+"actionSave" = "保存";
+
+// Tunnels list UI
+
+"tunnelsListTitle" = "WireGuard";
+"tunnelsListSettingsButtonTitle" = "設定";
+"tunnelsListCenteredAddTunnelButtonTitle" = "トンネルの追加";
+"tunnelsListSwipeDeleteButtonTitle" = "削除";
+"tunnelsListSelectButtonTitle" = "選択";
+"tunnelsListSelectAllButtonTitle" = "すべて選択";
+"tunnelsListDeleteButtonTitle" = "削除";
+"tunnelsListSelectedTitle (%d)" = "%d 件選択";
+
+// Tunnels list menu
+
+"addTunnelMenuHeader" = "WireGuardトンネルの追加";
+"addTunnelMenuImportFile" = "ファイル、アーカイブから作成";
+"addTunnelMenuQRCode" = "QRコードから作成";
+"addTunnelMenuFromScratch" = "空の状態から作成";
+
+// Tunnels list alerts
+
+"alertImportedFromMultipleFilesTitle (%d)" = "%d トンネルを作成しました";
+"alertImportedFromMultipleFilesMessage (%1$d of %2$d)" = "インポートファイルの %2$d 件中の %1$d トンネルを作成しました";
+
+"alertImportedFromZipTitle (%d)" = "%d トンネルを作成しました";
+"alertImportedFromZipMessage (%1$d of %2$d)" = "ZIP アーカイブの %2$d 件中の %1$d トンネルを作成しました";
+
+"alertBadConfigImportTitle" = "トンネルをインポートできません";
+"alertBadConfigImportMessage (%@)" = "ファイル ‘%@’ には有効な WireGuard の設定がありません";
+
+"deleteTunnelsConfirmationAlertButtonTitle" = "削除";
+"deleteTunnelConfirmationAlertButtonMessage (%d)" = "%d トンネルを削除しますか?";
+"deleteTunnelsConfirmationAlertButtonMessage (%d)" = "%d トンネルを削除しますか?";
+
+// Tunnel detail and edit UI
+
+"newTunnelViewTitle" = "新規作成";
+"editTunnelViewTitle" = "設定の編集";
+
+"tunnelSectionTitleStatus" = "状態";
+
+"tunnelStatusInactive" = "無効";
+"tunnelStatusActivating" = "有効化中";
+"tunnelStatusActive" = "有効";
+"tunnelStatusDeactivating" = "無効化中";
+"tunnelStatusReasserting" = "再有効化中";
+"tunnelStatusRestarting" = "再起動中";
+"tunnelStatusWaiting" = "待機中";
+
+"macToggleStatusButtonActivate" = "有効化";
+"macToggleStatusButtonActivating" = "有効化中…";
+"macToggleStatusButtonDeactivate" = "無効化";
+"macToggleStatusButtonDeactivating" = "無効化中…";
+"macToggleStatusButtonReasserting" = "再有効化中…";
+"macToggleStatusButtonRestarting" = "再起動中…";
+"macToggleStatusButtonWaiting" = "待機中…";
+
+"tunnelSectionTitleInterface" = "インターフェース";
+
+"tunnelInterfaceName" = "名前";
+"tunnelInterfacePrivateKey" = "秘密鍵";
+"tunnelInterfacePublicKey" = "公開鍵";
+"tunnelInterfaceGenerateKeypair" = "キーペアの生成";
+"tunnelInterfaceAddresses" = "IPアドレス";
+"tunnelInterfaceListenPort" = "待受ポート";
+"tunnelInterfaceMTU" = "MTU";
+"tunnelInterfaceDNS" = "DNS サーバ";
+"tunnelInterfaceStatus" = "状態";
+
+"tunnelSectionTitlePeer" = "ピア";
+
+"tunnelPeerPublicKey" = "公開鍵";
+"tunnelPeerPreSharedKey" = "事前共有鍵";
+"tunnelPeerEndpoint" = "エンドポイント";
+"tunnelPeerPersistentKeepalive" = "持続的キープアライブ";
+"tunnelPeerAllowedIPs" = "Allowed IPs";
+"tunnelPeerRxBytes" = "受信したデータ";
+"tunnelPeerTxBytes" = "送信したデータ";
+"tunnelPeerLastHandshakeTime" = "直近のハンドシェイク";
+"tunnelPeerExcludePrivateIPs" = "プライベートIPを対象外にする";
+
+"tunnelSectionTitleOnDemand" = "オンデマンド有効化";
+
+"tunnelOnDemandCellular" = "モバイル回線";
+"tunnelOnDemandEthernet" = "イーサネット(有線LAN)";
+"tunnelOnDemandWiFi" = "Wi-Fi";
+"tunnelOnDemandSSIDsKey" = "SSID";
+
+"tunnelOnDemandAnySSID" = "すべてのSSID";
+"tunnelOnDemandOnlyTheseSSIDs" = "これらのSSIDのみ";
+"tunnelOnDemandExceptTheseSSIDs" = "これらのSSIDを除外";
+"tunnelOnDemandOnlySSID (%d)" = "%d 件のSSIDのみ";
+"tunnelOnDemandOnlySSIDs (%d)" = "%d 件のSSIDのみ";
+"tunnelOnDemandExceptSSID (%d)" = "%d 件のSSIDが対象外";
+"tunnelOnDemandExceptSSIDs (%d)" = "%d 件のSSIDが対象外";
+"tunnelOnDemandSSIDOptionDescriptionMac (%1$@: %2$@)" = "%1$@: %2$@";
+
+"tunnelOnDemandSSIDViewTitle" = "SSID";
+"tunnelOnDemandSectionTitleSelectedSSIDs" = "SSID";
+"tunnelOnDemandNoSSIDs" = "SSIDなし";
+"tunnelOnDemandSectionTitleAddSSIDs" = "SSIDの追加";
+"tunnelOnDemandAddMessageAddConnectedSSID (%@)" = "接続中の %@ を追加";
+"tunnelOnDemandAddMessageAddNewSSID" = "追加";
+
+"tunnelOnDemandKey" = "オンデマンド";
+"tunnelOnDemandOptionOff" = "オフ";
+"tunnelOnDemandOptionWiFiOnly" = "Wi-Fi のみ";
+"tunnelOnDemandOptionWiFiOrCellular" = "Wi-Fi または モバイル回線";
+"tunnelOnDemandOptionCellularOnly" = "モバイル回線のみ";
+"tunnelOnDemandOptionWiFiOrEthernet" = "Wi-Fi または 有線LAN";
+"tunnelOnDemandOptionEthernetOnly" = "有線LANのみ";
+
+"addPeerButtonTitle" = "ピアを追加";
+
+"deletePeerButtonTitle" = "ピアを削除";
+"deletePeerConfirmationAlertButtonTitle" = "削除";
+"deletePeerConfirmationAlertMessage" = "このピアを削除しますか?";
+
+"deleteTunnelButtonTitle" = "トンネルを削除";
+"deleteTunnelConfirmationAlertButtonTitle" = "削除";
+"deleteTunnelConfirmationAlertMessage" = "このトンネルを削除しますか?";
+
+"tunnelEditPlaceholderTextRequired" = "必須";
+"tunnelEditPlaceholderTextOptional" = "任意";
+"tunnelEditPlaceholderTextAutomatic" = "自動";
+"tunnelEditPlaceholderTextStronglyRecommended" = "設定を強く推奨";
+"tunnelEditPlaceholderTextOff" = "オフ";
+
+"tunnelPeerPersistentKeepaliveValue (%@)" = "%@ 秒ごと";
+"tunnelHandshakeTimestampNow" = "今";
+"tunnelHandshakeTimestampSystemClockBackward" = "(システムクロックが巻き戻った)";
+"tunnelHandshakeTimestampAgo (%@)" = "%@ 前";
+"tunnelHandshakeTimestampYear (%d)" = "%d 年";
+"tunnelHandshakeTimestampYears (%d)" = "%d 年";
+"tunnelHandshakeTimestampDay (%d)" = "%d 日";
+"tunnelHandshakeTimestampDays (%d)" = "%d 日";
+"tunnelHandshakeTimestampHour (%d)" = "%d 時間";
+"tunnelHandshakeTimestampHours (%d)" = "%d 時間";
+"tunnelHandshakeTimestampMinute (%d)" = "%d 分";
+"tunnelHandshakeTimestampMinutes (%d)" = "%d 分";
+"tunnelHandshakeTimestampSecond (%d)" = "%d 秒";
+"tunnelHandshakeTimestampSeconds (%d)" = "%d 秒";
+
+"tunnelHandshakeTimestampHours hh:mm:ss (%@)" = "%@ 時間";
+"tunnelHandshakeTimestampMinutes mm:ss (%@)" = "%@ 分";
+
+"tunnelPeerPresharedKeyEnabled" = "有効";
+
+// Error alerts while creating / editing a tunnel configuration
+/* Alert title for error in the interface data */
+
+"alertInvalidInterfaceTitle" = "無効なインターフェース";
+
+/* Any one of the following alert messages can go with the above title */
+"alertInvalidInterfaceMessageNameRequired" = "インターフェース名は必須の設定項目です";
+"alertInvalidInterfaceMessagePrivateKeyRequired" = "インターフェースの秘密鍵は必須の設定項目です";
+"alertInvalidInterfaceMessagePrivateKeyInvalid" = "インターフェースの秘密鍵はBase64でエンコードされた32バイトの長さでなければなりません";
+"alertInvalidInterfaceMessageAddressInvalid" = "インターフェースのアドレスはカンマで区切られたIPアドレス(CIDR表記可)のリストでなければなりません";
+"alertInvalidInterfaceMessageListenPortInvalid" = "インターフェースの待受ポートは0から65535の範囲内の値か、または未指定でなければなりません";
+"alertInvalidInterfaceMessageMTUInvalid" = "インターフェースのMTUは576から65535の範囲内の値か、または未指定でなければなりません";
+"alertInvalidInterfaceMessageDNSInvalid" = "インターフェースのDNSサーバはカンマで区切られたIPアドレスのリストでなければなりません";
+
+/* Alert title for error in the peer data */
+"alertInvalidPeerTitle" = "無効なピア";
+
+/* Any one of the following alert messages can go with the above title */
+"alertInvalidPeerMessagePublicKeyRequired" = "ピアの公開鍵は必須の設定項目です";
+"alertInvalidPeerMessagePublicKeyInvalid" = "ピアの公開鍵はBase64でエンコードされた32バイトの長さでなければなりません";
+"alertInvalidPeerMessagePreSharedKeyInvalid" = "ピアの事前共有鍵はBase64でエンコードされた32バイトの長さでなければなりません";
+"alertInvalidPeerMessageAllowedIPsInvalid" = "ピアのAllowed IPはカンマで区切られたIPアドレス(CIDR表記可)のリストでなければなりません";
+"alertInvalidPeerMessageEndpointInvalid" = "ピアのエンドポイントは 'ホスト:ポート番号' または '[ホスト]:ポート番号' の形式でなければなりません’";
+"alertInvalidPeerMessagePersistentKeepaliveInvalid" = "ピアの持続的キープアライブは0から65535の範囲内の値か、または未指定でなければなりません";
+"alertInvalidPeerMessagePublicKeyDuplicated" = "複数のピアで同じ公開鍵は使用できません";
+
+// Scanning QR code UI
+
+"scanQRCodeViewTitle" = "QRコードをスキャン";
+"scanQRCodeTipText" = "Tip: `qrencode -t ansiutf8 < tunnel.conf` で生成できます";
+
+// Scanning QR code alerts
+
+"alertScanQRCodeCameraUnsupportedTitle" = "カメラがサポートされていません";
+"alertScanQRCodeCameraUnsupportedMessage" = "この機器ではQRコードをスキャンできません";
+
+"alertScanQRCodeInvalidQRCodeTitle" = "無効なQRコード";
+"alertScanQRCodeInvalidQRCodeMessage" = "スキャンしたQRコードは有効なWireGuardの設定ではありません";
+
+"alertScanQRCodeUnreadableQRCodeTitle" = "無効なコード";
+"alertScanQRCodeUnreadableQRCodeMessage" = "スキャンしたコードが解読できません";
+
+"alertScanQRCodeNamePromptTitle" = "スキャンしたトンネル設定に名前をつけてください";
+
+// Settings UI
+
+"settingsViewTitle" = "設定";
+
+"settingsSectionTitleAbout" = "このプログラムについて";
+"settingsVersionKeyWireGuardForIOS" = "WireGuard for iOS";
+"settingsVersionKeyWireGuardGoBackend" = "WireGuard Go Backend";
+
+"settingsSectionTitleExportConfigurations" = "設定のエクスポート";
+"settingsExportZipButtonTitle" = "ZIPアーカイブとしてエクスポート";
+
+"settingsSectionTitleTunnelLog" = "ログ";
+"settingsViewLogButtonTitle" = "ログを参照する";
+
+// Log view
+
+"logViewTitle" = "ログ";
+
+// Log alerts
+
+"alertUnableToRemovePreviousLogTitle" = "ログのエクスポートに失敗";
+"alertUnableToRemovePreviousLogMessage" = "以前のログを消去できませんでした";
+
+"alertUnableToWriteLogTitle" = "ログのエクスポートに失敗";
+"alertUnableToWriteLogMessage" = "ファイルにログを書き込めません";
+
+// Zip import / export error alerts
+
+"alertCantOpenInputZipFileTitle" = "ZIPアーカイブの読込不可";
+"alertCantOpenInputZipFileMessage" = "ZIPアーカイブが読み込めません";
+
+"alertCantOpenOutputZipFileForWritingTitle" = "ZIPアーカイブ作成不可";
+"alertCantOpenOutputZipFileForWritingMessage" = "ZIPファイルを書き込み用に開けません";
+
+"alertBadArchiveTitle" = "ZIPアーカイブの読込不可";
+"alertBadArchiveMessage" = "不良なZIPアーカイブファイルです";
+
+"alertNoTunnelsToExportTitle" = "エクスポート対象なし";
+"alertNoTunnelsToExportMessage" = "エクスポートできるトンネルが1つもありません";
+
+"alertNoTunnelsInImportedZipArchiveTitle" = "ZIPアーカイブにトンネル設定がない";
+"alertNoTunnelsInImportedZipArchiveMessage" = "ZIPアーカイブ内に拡張子 .conf のトンネル設定ファイルが見つかりません";
+
+// Conf import error alerts
+
+"alertCantOpenInputConfFileTitle" = "ファイルからのインポート不可";
+"alertCantOpenInputConfFileMessage (%@)" = "ファイル ‘%@’ は読み込めません";
+
+// Tunnel management error alerts
+
+"alertTunnelActivationFailureTitle" = "有効化に失敗";
+"alertTunnelActivationFailureMessage" = "トンネルを有効化できませんでした。インターネットに接続しているか確認してください。";
+"alertTunnelActivationSavedConfigFailureMessage" = "保存済みの設定からトンネルの情報を取得できませんでした";
+"alertTunnelActivationBackendFailureMessage" = "Go バックエンドライブラリを起動できません";
+"alertTunnelActivationFileDescriptorFailureMessage" = "TUN デバイスのファイルディスクリプタを特定できません";
+"alertTunnelActivationSetNetworkSettingsMessage" = "トンネルオブジェクトにネットワーク設定を適用できません";
+
+"alertTunnelActivationFailureOnDemandAddendum" = " このトンネルは「オンデマンド有効化」が有効なため、OSによって自動的に有効化が行われます。このアプリのトンネル設定から「オンデマンド有効化」をオフにできます。";
+
+"alertTunnelDNSFailureTitle" = "DNSによる名前解決失敗";
+"alertTunnelDNSFailureMessage" = "エンドポイントのドメイン名が解決できませんでした";
+
+"alertTunnelNameEmptyTitle" = "名前が未指定";
+"alertTunnelNameEmptyMessage" = "名前のないトンネルは作成できません";
+
+"alertTunnelAlreadyExistsWithThatNameTitle" = "存在する名前";
+"alertTunnelAlreadyExistsWithThatNameMessage" = "同じ名前のトンネルがすでに存在します";
+
+"alertTunnelActivationErrorTunnelIsNotInactiveTitle" = "有効化中";
+"alertTunnelActivationErrorTunnelIsNotInactiveMessage" = "トンネルはすでに有効になっているか、有効化処理中です";
+
+// Tunnel management error alerts on system error
+
+/* The alert message that goes with the following titles would be
+ one of the alertSystemErrorMessage* listed further down */
+"alertSystemErrorOnListingTunnelsTitle" = "トンネル表示不可";
+"alertSystemErrorOnAddTunnelTitle" = "トンネル作成不可";
+"alertSystemErrorOnModifyTunnelTitle" = "トンネル編集不可";
+"alertSystemErrorOnRemoveTunnelTitle" = "トンネル削除不可";
+
+/* The alert message for this alert shall include
+ one of the alertSystemErrorMessage* listed further down */
+"alertTunnelActivationSystemErrorTitle" = "有効化に失敗";
+"alertTunnelActivationSystemErrorMessage (%@)" = "トンネルを有効化できませんでした。 %@";
+
+/* alertSystemErrorMessage* messages */
+"alertSystemErrorMessageTunnelConfigurationInvalid" = "設定が不正です";
+"alertSystemErrorMessageTunnelConfigurationDisabled" = "設定が無効化されています。";
+"alertSystemErrorMessageTunnelConnectionFailed" = "接続に失敗しました。";
+"alertSystemErrorMessageTunnelConfigurationStale" = "設定が他のプロセスによって更新されています";
+"alertSystemErrorMessageTunnelConfigurationReadWriteFailed" = "設定の読み取り、または書き込みに失敗しました。";
+"alertSystemErrorMessageTunnelConfigurationUnknown" = "不明なシステムエラー。";
+
+// Mac status bar menu / pulldown menu / main menu
+
+"macMenuNetworks (%@)" = "ネットワーク: %@";
+"macMenuNetworksNone" = "ネットワーク: なし";
+
+"macMenuTitle" = "WireGuard";
+"macMenuManageTunnels" = "トンネルの管理";
+"macMenuImportTunnels" = "ファイルからトンネルをインポート…";
+"macMenuAddEmptyTunnel" = "設定が空のトンネルを追加…";
+"macMenuViewLog" = "ログの参照";
+"macMenuExportTunnels" = "トンネル設定をZIPにエクスポート…";
+"macMenuAbout" = "WireGuard について";
+"macMenuQuit" = "WireGuardを終了";
+
+"macMenuHideApp" = "WireGuard を隠す";
+"macMenuHideOtherApps" = "ほかを隠す";
+"macMenuShowAllApps" = "すべてを表示";
+
+"macMenuFile" = "ファイル";
+"macMenuCloseWindow" = "ウィンドウを閉じる";
+
+"macMenuEdit" = "編集";
+"macMenuCut" = "切り取り";
+"macMenuCopy" = "コピー";
+"macMenuPaste" = "貼り付け";
+"macMenuSelectAll" = "すべてを選択";
+
+"macMenuTunnel" = "トンネル";
+"macMenuToggleStatus" = "ステータスを切り替え";
+"macMenuEditTunnel" = "編集…";
+"macMenuDeleteSelected" = "選択したものを削除";
+
+"macMenuWindow" = "ウィンドウ";
+"macMenuMinimize" = "最小化";
+"macMenuZoom" = "ズーム";
+
+// Mac manage tunnels window
+
+"macWindowTitleManageTunnels" = "WireGuardトンネルの管理";
+
+"macDeleteTunnelConfirmationAlertMessage (%@)" = "本当に ‘%@’ を削除しますか?";
+"macDeleteMultipleTunnelsConfirmationAlertMessage (%d)" = "本当に %d トンネルを削除しますか?";
+"macDeleteTunnelConfirmationAlertInfo" = "この操作はもとに戻せません。";
+"macDeleteTunnelConfirmationAlertButtonTitleDelete" = "削除";
+"macDeleteTunnelConfirmationAlertButtonTitleCancel" = "キャンセル";
+"macDeleteTunnelConfirmationAlertButtonTitleDeleting" = "削除しています…";
+
+"macButtonImportTunnels" = "ファイルからトンネルをインポート";
+"macSheetButtonImport" = "インポート";
+
+"macNameFieldExportLog" = "ログの保存先:";
+"macSheetButtonExportLog" = "保存";
+
+"macNameFieldExportZip" = "トンネルのエクスポート先:";
+"macSheetButtonExportZip" = "保存";
+
+"macButtonDeleteTunnels (%d)" = "%d トンネルを削除";
+
+"macButtonEdit" = "編集";
+
+// Mac detail/edit view fields
+
+"macFieldKey (%@)" = "%@:";
+"macFieldOnDemand" = "オンデマンド:";
+"macFieldOnDemandSSIDs" = "SSID:";
+
+// Mac status display
+
+"macStatus (%@)" = "状態: %@";
+
+// Mac editing config
+
+"macEditDiscard" = "破棄";
+"macEditSave" = "保存";
+
+"macAlertNameIsEmpty" = "名前は必須です";
+"macAlertDuplicateName (%@)" = "‘%@’ という名前のトンネルはすでに存在します。";
+
+"macAlertInvalidLine (%@)" = "不正な行: ‘%@’。";
+
+"macAlertNoInterface" = "設定には ‘Interface’ セクションが必須です";
+"macAlertMultipleInterfaces" = "設定内の 'Interface' セクションは1つだけです";
+"macAlertPrivateKeyInvalid" = "秘密鍵が不正です。";
+"macAlertListenPortInvalid (%@)" = "待受ポート ‘%@’ は無効です。";
+"macAlertAddressInvalid (%@)" = "アドレス ‘%@’ は無効です。";
+"macAlertDNSInvalid (%@)" = "DNS ‘%@’ は無効です。";
+"macAlertMTUInvalid (%@)" = "MTU ‘%@’ は無効です。";
+
+"macAlertUnrecognizedInterfaceKey (%@)" = "Interfaceに認識不能なキー項目 ‘%@’ が含まれています";
+"macAlertInfoUnrecognizedInterfaceKey" = "有効なキー項目: ‘PrivateKey’, ‘ListenPort’, ‘Address’, ‘DNS’, ‘MTU’";
+
+"macAlertPublicKeyInvalid" = "公開鍵が不正です";
+"macAlertPreSharedKeyInvalid" = "事前共有鍵が不正です";
+"macAlertAllowedIPInvalid (%@)" = "Allowed IP ‘%@’ は無効です";
+"macAlertEndpointInvalid (%@)" = "Endpoint ‘%@’ は無効です";
+"macAlertPersistentKeepliveInvalid (%@)" = "持続的キープアライブの値 ‘%@’ は無効です";
+
+"macAlertUnrecognizedPeerKey (%@)" = "Peerに認識不能なキー項目 ‘%@’ が含まれています";
+"macAlertInfoUnrecognizedPeerKey" = "有効なキー項目: ‘PublicKey’, ‘PresharedKey’, ‘AllowedIPs’, ‘Endpoint’, ‘PersistentKeepalive’";
+
+"macAlertMultipleEntriesForKey (%@)" = "キー項目 ‘%@’ は各セクションに1エントリだけにします";
+
+// Mac about dialog
+
+"macAppVersion (%@)" = "App version: %@";
+"macGoBackendVersion (%@)" = "Go backend version: %@";
+
+// Privacy
+
+"macExportPrivateData" = "トンネルの秘密鍵をエクスポート";
+"macViewPrivateData" = "トンネルの秘密鍵を表示";
+"iosExportPrivateData" = "トンネルの秘密鍵をエクスポートするために認証を行います";
+"iosViewPrivateData" = "トンネルの秘密鍵を表示するために認証を行います";
+
+// Mac alert
+
+"macConfirmAndQuitAlertMessage" = "トンネル管理画面を閉じますか?それともWireGuard自体を終了しますか?";
+"macConfirmAndQuitAlertInfo" = "トンネル管理画面を閉じても、メニューバーアイコンからWireGuardを操作できます。";
+"macConfirmAndQuitInfoWithActiveTunnel (%@)" = "トンネル管理画面を閉じても、メニューバーアイコンからWireGuardを操作できます。\n\n注意:WireGuard自体を終了しても、現在有効化済みのトンネル ('%@') はこのアプリケーションかシステム環境設定のネットワークパネルから無効化するまでは有効なままです。";
+"macConfirmAndQuitAlertQuitWireGuard" = "WireGuardを終了";
+"macConfirmAndQuitAlertCloseWindow" = "トンネル管理画面を閉じる";
+
+"macAppExitingWithActiveTunnelMessage" = "有効化中のトンネルが残ったまま WireGuard を終了しようとしています";
+"macAppExitingWithActiveTunnelInfo" = "終了後もトンネルは有効なままです。無効化はこのアプリケーションを再度起動して行うか、システム環境設定のネットワークパネルから行います。";
+
+// Mac tooltip
+
+"macToolTipEditTunnel" = "トンネルの編集 (⌘E)";
+"macToolTipToggleStatus" = "状態の切り替え (⌘T)";
+
+// Mac log view
+
+"macLogColumnTitleTime" = "時刻";
+"macLogColumnTitleLogMessage" = "ログメッセージ";
+"macLogButtonTitleClose" = "閉じる";
+"macLogButtonTitleSave" = "保存…";
+
+// Mac unusable tunnel view
+
+"macUnusableTunnelMessage" = "このトンネルの設定がキーチェーン内に見つかりません。";
+"macUnusableTunnelInfo" = "このトンネルが他のユーザーによって作成されたものである場合、そのユーザーだけがこのトンネルの表示、編集、有効化ができます。";
+"macUnusableTunnelButtonTitleDeleteTunnel" = "トンネルを削除";
+
+// Mac App Store updating alert
+
+"macAppStoreUpdatingAlertMessage" = "App Store に WireGuard の更新版があります";
+"macAppStoreUpdatingAlertInfoWithOnDemand (%@)" = "トンネル '%@' のオンデマンド有効化を解除し、トンネルを無効にしてから App Store での更新を行ってください。";
+"macAppStoreUpdatingAlertInfoWithoutOnDemand (%@)" = "トンネル ‘%@’ を無効にしてから App Store での更新を行ってください。";
+
+// Donation
+
+"donateLink" = "♥ WireGuard プロジェクトに寄付する";