183 lines
6.7 KiB
Swift
183 lines
6.7 KiB
Swift
|
|
//
|
||
|
|
// TetheringHiderApp.swift
|
||
|
|
//
|
||
|
|
//
|
||
|
|
// Created by Jesús David Chapman Vélez on 12/08/26.
|
||
|
|
//
|
||
|
|
|
||
|
|
import SwiftUI
|
||
|
|
import AppKit
|
||
|
|
|
||
|
|
@MainActor
|
||
|
|
final class StatusBarController: NSObject, NSMenuDelegate {
|
||
|
|
private var statusItem: NSStatusItem?
|
||
|
|
private let ttlManager: TTLManager
|
||
|
|
private let authService = AuthenticationService()
|
||
|
|
private let commandService = SystemCommandService()
|
||
|
|
|
||
|
|
init(ttlManager: TTLManager) {
|
||
|
|
self.ttlManager = ttlManager
|
||
|
|
super.init()
|
||
|
|
setupStatusItem()
|
||
|
|
|
||
|
|
self.ttlManager.onStatusChanged = { [weak self] in
|
||
|
|
Task { @MainActor in
|
||
|
|
self?.updateMenu()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func setupStatusItem() {
|
||
|
|
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
||
|
|
updateMenu()
|
||
|
|
}
|
||
|
|
|
||
|
|
func updateMenu() {
|
||
|
|
guard let statusItem = statusItem, let button = statusItem.button else { return }
|
||
|
|
|
||
|
|
// Status Bar Icon
|
||
|
|
let imageName = ttlManager.isProtected
|
||
|
|
? "antenna.radiowaves.left.and.right"
|
||
|
|
: "antenna.radiowaves.left.and.right.slash"
|
||
|
|
|
||
|
|
button.image = NSImage(systemSymbolName: imageName, accessibilityDescription: "Tethering Hider")
|
||
|
|
|
||
|
|
let menu = NSMenu()
|
||
|
|
menu.delegate = self
|
||
|
|
|
||
|
|
let statusTitle = ttlManager.isProtected
|
||
|
|
? String(localized: "Tethering Hider — Hotspot Cloaked")
|
||
|
|
: String(localized: "Tethering Hider — Standard Tethering")
|
||
|
|
|
||
|
|
let headerItem = NSMenuItem(title: statusTitle, action: nil, keyEquivalent: "")
|
||
|
|
headerItem.isEnabled = false
|
||
|
|
headerItem.image = NSImage(
|
||
|
|
systemSymbolName: ttlManager.isProtected ? "eye.slash.fill" : "eye.fill",
|
||
|
|
accessibilityDescription: nil
|
||
|
|
)
|
||
|
|
menu.addItem(headerItem)
|
||
|
|
|
||
|
|
menu.addItem(NSMenuItem.separator())
|
||
|
|
|
||
|
|
let toggleTitle = ttlManager.isProtected
|
||
|
|
? String(localized: "Disable Hotspot Cloak (Set TTL 64)")
|
||
|
|
: String(localized: "Enable Hotspot Cloak (Set TTL 65)")
|
||
|
|
|
||
|
|
let toggleItem = NSMenuItem(title: toggleTitle, action: #selector(toggleAction), keyEquivalent: "")
|
||
|
|
toggleItem.target = self
|
||
|
|
toggleItem.image = NSImage(
|
||
|
|
systemSymbolName: ttlManager.isProtected ? "eye.fill" : "eye.slash.fill",
|
||
|
|
accessibilityDescription: nil
|
||
|
|
)
|
||
|
|
menu.addItem(toggleItem)
|
||
|
|
|
||
|
|
menu.addItem(NSMenuItem.separator())
|
||
|
|
|
||
|
|
let ipv4Format = String(localized: "IPv4 TTL: %d")
|
||
|
|
let ipv4Item = NSMenuItem(title: String(format: ipv4Format, ttlManager.currentTTL), action: nil, keyEquivalent: "")
|
||
|
|
ipv4Item.isEnabled = false
|
||
|
|
ipv4Item.image = NSImage(systemSymbolName: "network", accessibilityDescription: nil)
|
||
|
|
menu.addItem(ipv4Item)
|
||
|
|
|
||
|
|
let ipv6Format = String(localized: "IPv6 HLIM: %d")
|
||
|
|
let ipv6Item = NSMenuItem(title: String(format: ipv6Format, ttlManager.currentIPv6HopLimit), action: nil, keyEquivalent: "")
|
||
|
|
ipv6Item.isEnabled = false
|
||
|
|
ipv6Item.image = NSImage(systemSymbolName: "globe", accessibilityDescription: nil)
|
||
|
|
menu.addItem(ipv6Item)
|
||
|
|
|
||
|
|
let dnsTitle = String(localized: "DNS Flush: AUTO")
|
||
|
|
let dnsItem = NSMenuItem(title: dnsTitle, action: nil, keyEquivalent: "")
|
||
|
|
dnsItem.isEnabled = false
|
||
|
|
dnsItem.image = NSImage(systemSymbolName: "arrow.triangle.2.circlepath", accessibilityDescription: nil)
|
||
|
|
menu.addItem(dnsItem)
|
||
|
|
|
||
|
|
menu.addItem(NSMenuItem.separator())
|
||
|
|
|
||
|
|
let aboutTitle = String(localized: "About Tethering Hider...")
|
||
|
|
let aboutItem = NSMenuItem(title: aboutTitle, action: #selector(aboutAction), keyEquivalent: "")
|
||
|
|
aboutItem.target = self
|
||
|
|
aboutItem.image = NSImage(systemSymbolName: "info.circle.fill", accessibilityDescription: nil)
|
||
|
|
menu.addItem(aboutItem)
|
||
|
|
|
||
|
|
menu.addItem(NSMenuItem.separator())
|
||
|
|
|
||
|
|
let quitTitle = String(localized: "Quit Tethering Hider")
|
||
|
|
let quitItem = NSMenuItem(title: quitTitle, action: #selector(quitAction), keyEquivalent: "q")
|
||
|
|
quitItem.target = self
|
||
|
|
quitItem.image = NSImage(systemSymbolName: "rectangle.portrait.and.arrow.right", accessibilityDescription: nil)
|
||
|
|
menu.addItem(quitItem)
|
||
|
|
|
||
|
|
statusItem.menu = menu
|
||
|
|
}
|
||
|
|
|
||
|
|
func menuWillOpen(_ menu: NSMenu) {
|
||
|
|
ttlManager.refreshStatus()
|
||
|
|
}
|
||
|
|
|
||
|
|
@objc private func toggleAction() {
|
||
|
|
Task { @MainActor in
|
||
|
|
do {
|
||
|
|
let authenticated = try await authService.authenticateWithBiometrics()
|
||
|
|
guard authenticated else { return }
|
||
|
|
|
||
|
|
if ttlManager.isProtected {
|
||
|
|
try await commandService.resetTTL()
|
||
|
|
ttlManager.didSetTTL(ttlManager.defaultTTL)
|
||
|
|
} else {
|
||
|
|
try await commandService.setTTL(ttlManager.targetTTL)
|
||
|
|
ttlManager.didSetTTL(ttlManager.targetTTL)
|
||
|
|
}
|
||
|
|
updateMenu()
|
||
|
|
} catch {
|
||
|
|
ttlManager.setError(error.localizedDescription)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@objc private func aboutAction() {
|
||
|
|
let creditsString = String(localized: "Bypasses mobile carrier hotspot tethering detection by adjusting outgoing IPv4 TTL to 65, IPv6 Hop Limit to 65, and flushing system DNS caches.\n\nDesigned for macOS.")
|
||
|
|
let options: [NSApplication.AboutPanelOptionKey: Any] = [
|
||
|
|
.credits: NSAttributedString(
|
||
|
|
string: creditsString,
|
||
|
|
attributes: [
|
||
|
|
.font: NSFont.systemFont(ofSize: 11),
|
||
|
|
.foregroundColor: NSColor.secondaryLabelColor
|
||
|
|
]
|
||
|
|
),
|
||
|
|
.applicationName: "Tethering Hider",
|
||
|
|
.version: "1.0.0"
|
||
|
|
]
|
||
|
|
NSApp.orderFrontStandardAboutPanel(options: options)
|
||
|
|
NSApp.activate(ignoringOtherApps: true)
|
||
|
|
}
|
||
|
|
|
||
|
|
@objc private func quitAction() {
|
||
|
|
NSApp.terminate(nil)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@MainActor
|
||
|
|
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||
|
|
var statusBarController: StatusBarController?
|
||
|
|
let ttlManager = TTLManager()
|
||
|
|
|
||
|
|
func applicationWillFinishLaunching(_ notification: Notification) {
|
||
|
|
NSApp.setActivationPolicy(.accessory)
|
||
|
|
}
|
||
|
|
|
||
|
|
func applicationDidFinishLaunching(_ notification: Notification) {
|
||
|
|
NSApp.setActivationPolicy(.accessory)
|
||
|
|
self.statusBarController = StatusBarController(ttlManager: ttlManager)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@main
|
||
|
|
struct TetheringHiderApp: App {
|
||
|
|
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||
|
|
|
||
|
|
var body: some Scene {
|
||
|
|
Settings {
|
||
|
|
EmptyView()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|