import Cocoa import WebKit import AuthenticationServices import CryptoKit import Security @MainActor private final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler { weak var controller: MainWindowController? init(controller: MainWindowController) { self.controller = controller } func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { if message.name == "shdDesktopAuth" { let origin = message.frameInfo.securityOrigin let trustedHosts = ["shd.xyz.su", "dev-shd.xyz.su"] guard message.frameInfo.isMainFrame, origin.protocol.lowercased() == "https", trustedHosts.contains(origin.host.lowercased()) else { return } let environment: MainWindowController.Environment = origin.host.lowercased() == "dev-shd.xyz.su" ? .dev : .production controller?.signInWithPasskeyInBrowser(for: environment) return } if message.name == "shdNativePrint" { controller?.printCurrentPage() } } } @MainActor final class MainWindowController: NSWindowController, NSWindowDelegate, WKNavigationDelegate, WKUIDelegate, WKDownloadDelegate, ASWebAuthenticationPresentationContextProviding { enum Environment: String, CaseIterable { case production = "https://shd.xyz.su" case dev = "https://dev-shd.xyz.su" var title: String { switch self { case .production: return "SHD Prod" case .dev: return "SHD Dev" } } } var webView: WKWebView! private var progressIndicator: NSProgressIndicator! private var currentEnv: Environment = .production private var observation: NSKeyValueObservation? private var authSession: ASWebAuthenticationSession? private var authAttemptID: UUID? private static let authClientID = "shd-desktop" private static let authCallbackScheme = "shd-auth" private static let authCallbackURI = "shd-auth://callback" init(initialEnv: Environment? = nil, initialRequest: URLRequest? = nil) { let window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 1440, height: 920), styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView], backing: .buffered, defer: false ) window.center() window.title = "SHD" window.titleVisibility = .hidden window.titlebarAppearsTransparent = true window.isReleasedWhenClosed = false window.minSize = NSSize(width: 800, height: 500) // Dark theme: ensures window and native tabs are always dark matching SHD window.appearance = NSAppearance(named: .darkAqua) let darkBg = NSColor(calibratedRed: 0.08, green: 0.08, blue: 0.08, alpha: 1.0) window.backgroundColor = darkBg // Native macOS tabs support window.tabbingMode = .preferred window.tabbingIdentifier = "SHDMainTabGroup" super.init(window: window) window.delegate = self setupWebView(darkBg: darkBg) if let initialRequest = initialRequest { webView.load(initialRequest) } else if let initialEnv = initialEnv { currentEnv = initialEnv navigateToEnvironment(currentEnv) } else { loadInitialEnvironment() } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setupWebView(darkBg: NSColor) { guard let window = window else { return } let config = WKWebViewConfiguration() config.websiteDataStore = WKWebsiteDataStore.default() config.preferences.isElementFullscreenEnabled = true config.defaultWebpagePreferences.allowsContentJavaScript = true // Immediate dark style injection before document parsing even starts let darkCSS = """ html, body { background-color: #121212 !important; color: #e0e0e0 !important; } """ let darkScript = """ (function() { var el = document.documentElement; if (el) { el.style.backgroundColor = '#121212'; } var style = document.createElement('style'); style.id = '__shd_instant_dark__'; style.textContent = '\(darkCSS.replacingOccurrences(of: "\n", with: " "))'; if (document.head) { document.head.appendChild(style); } else if (el) { el.appendChild(style); } window.print = function() { if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.shdNativePrint) { window.webkit.messageHandlers.shdNativePrint.postMessage({}); } }; })(); """ let userScript = WKUserScript(source: darkScript, injectionTime: .atDocumentStart, forMainFrameOnly: false) config.userContentController.addUserScript(userScript) let passkeyBridgeScript = """ (function() { document.addEventListener('click', function(event) { var path = window.location.pathname; while (path.length > 1 && path.endsWith('/')) { path = path.slice(0, -1); } if (path !== '/login' && path !== '/login/two-factor') { return; } var target = event.target instanceof Element ? event.target : (event.target && event.target.parentElement); if (!target || !target.closest('#btnPasskeyLogin, #twoFactorPasskey')) { return; } event.preventDefault(); event.stopPropagation(); event.stopImmediatePropagation(); var bridge = window.webkit && window.webkit.messageHandlers ? window.webkit.messageHandlers.shdDesktopAuth : null; if (bridge) { bridge.postMessage('passkey-login'); } }, true); })(); """ config.userContentController.addUserScript( WKUserScript(source: passkeyBridgeScript, injectionTime: .atDocumentStart, forMainFrameOnly: true) ) config.userContentController.add(WeakScriptMessageHandler(controller: self), name: "shdNativePrint") config.userContentController.add(WeakScriptMessageHandler(controller: self), name: "shdDesktopAuth") let container = NSView() container.translatesAutoresizingMaskIntoConstraints = false container.wantsLayer = true container.layer?.backgroundColor = darkBg.cgColor window.contentView = container webView = WKWebView(frame: .zero, configuration: config) webView.translatesAutoresizingMaskIntoConstraints = false webView.navigationDelegate = self webView.uiDelegate = self webView.allowsBackForwardNavigationGestures = true webView.isInspectable = true webView.underPageBackgroundColor = darkBg webView.setValue(false, forKey: "drawsBackground") // Prevents WebKit initial white canvas container.addSubview(webView) progressIndicator = NSProgressIndicator() progressIndicator.isIndeterminate = false progressIndicator.style = .bar progressIndicator.minValue = 0.0 progressIndicator.maxValue = 1.0 progressIndicator.translatesAutoresizingMaskIntoConstraints = false progressIndicator.isDisplayedWhenStopped = false container.addSubview(progressIndicator) // Layout: Pin webview to container but respect safe area so tab bar does NOT overlap topbar NSLayoutConstraint.activate([ webView.leadingAnchor.constraint(equalTo: container.leadingAnchor), webView.trailingAnchor.constraint(equalTo: container.trailingAnchor), webView.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor), webView.bottomAnchor.constraint(equalTo: container.bottomAnchor), progressIndicator.leadingAnchor.constraint(equalTo: container.leadingAnchor), progressIndicator.trailingAnchor.constraint(equalTo: container.trailingAnchor), progressIndicator.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor), progressIndicator.heightAnchor.constraint(equalToConstant: 2.0) ]) observation = webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in Task { @MainActor [weak self] in guard let self = self else { return } self.progressIndicator.doubleValue = webView.estimatedProgress if webView.estimatedProgress >= 1.0 { self.progressIndicator.stopAnimation(nil) self.progressIndicator.isHidden = true } else { self.progressIndicator.isHidden = false self.progressIndicator.startAnimation(nil) } self.updateTitle() } } } private func loadInitialEnvironment() { let savedEnvRaw = UserDefaults.standard.string(forKey: "SHDSelectedEnvironment") if let savedEnvRaw = savedEnvRaw, let env = Environment(rawValue: savedEnvRaw) { currentEnv = env } else { currentEnv = .production } navigateToEnvironment(currentEnv) } func navigateToEnvironment(_ env: Environment) { currentEnv = env UserDefaults.standard.set(env.rawValue, forKey: "SHDSelectedEnvironment") if let url = URL(string: env.rawValue) { let request = URLRequest(url: url) webView.load(request) } updateTitle() } func signInWithPasskeyInBrowser(for environment: Environment) { guard authSession == nil else { return } guard environment == .dev else { presentDesktopAuthError("Вход по passkey в приложении пока подключён только для DEV.") return } do { let verifier = try Self.randomBase64URLString(byteCount: 32) let state = try Self.randomBase64URLString(byteCount: 32) let challenge = Self.base64URLString(for: Data(SHA256.hash(data: Data(verifier.utf8)))) guard let authorizationURL = makeAuthorizationURL( environment: environment, state: state, challenge: challenge ) else { presentDesktopAuthError("Не удалось подготовить адрес входа.") return } let attemptID = UUID() authAttemptID = attemptID let completionHandler: ASWebAuthenticationSession.CompletionHandler = { [weak self] callbackURL, error in Task { @MainActor [weak self] in self?.finishBrowserPasskeyLogin( callbackURL: callbackURL, error: error, expectedState: state, verifier: verifier, environment: environment, attemptID: attemptID ) } } let session: ASWebAuthenticationSession if #available(macOS 14.4, *) { session = ASWebAuthenticationSession( url: authorizationURL, callback: .customScheme(Self.authCallbackScheme), completionHandler: completionHandler ) } else { session = ASWebAuthenticationSession( url: authorizationURL, callbackURLScheme: Self.authCallbackScheme, completionHandler: completionHandler ) } session.presentationContextProvider = self authSession = session if !session.start() { authSession = nil authAttemptID = nil presentDesktopAuthError("macOS не смог открыть окно входа. Попробуйте ещё раз.") } } catch { presentDesktopAuthError("Не удалось безопасно подготовить вход.") } } private func makeAuthorizationURL(environment: Environment, state: String, challenge: String) -> URL? { guard var components = URLComponents(string: environment.rawValue) else { return nil } components.path = "/desktop-auth/authorize" components.queryItems = [ URLQueryItem(name: "client_id", value: Self.authClientID), URLQueryItem(name: "redirect_uri", value: Self.authCallbackURI), URLQueryItem(name: "response_type", value: "code"), URLQueryItem(name: "state", value: state), URLQueryItem(name: "code_challenge", value: challenge), URLQueryItem(name: "code_challenge_method", value: "S256") ] return components.url } private func finishBrowserPasskeyLogin( callbackURL: URL?, error: Error?, expectedState: String, verifier: String, environment: Environment, attemptID: UUID ) { guard authAttemptID == attemptID else { return } authAttemptID = nil authSession = nil if let error { if (error as? ASWebAuthenticationSessionError)?.code != .canceledLogin { presentDesktopAuthError("Вход через браузер не завершился. Попробуйте ещё раз.") } return } guard let callbackURL, callbackURL.scheme?.lowercased() == Self.authCallbackScheme, callbackURL.host?.lowercased() == "callback", let callbackComponents = URLComponents(url: callbackURL, resolvingAgainstBaseURL: false), let queryItems = callbackComponents.queryItems else { presentDesktopAuthError("Сервер вернул некорректный ответ входа.") return } if queryItems.contains(where: { $0.name == "error" }) { presentDesktopAuthError("Сервер не завершил вход. Попробуйте ещё раз.") return } let stateValues = queryItems.filter { $0.name == "state" } let codeValues = queryItems.filter { $0.name == "code" } guard stateValues.count == 1, stateValues[0].value == expectedState, codeValues.count == 1, let code = codeValues[0].value, !code.isEmpty else { presentDesktopAuthError("Не удалось подтвердить ответ сервера. Начните вход заново.") return } guard var completeComponents = URLComponents(string: environment.rawValue) else { presentDesktopAuthError("Не удалось завершить вход в выбранном контуре.") return } completeComponents.path = "/desktop-auth/complete" guard let completeURL = completeComponents.url else { presentDesktopAuthError("Не удалось завершить вход в выбранном контуре.") return } var request = URLRequest(url: completeURL, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30) request.httpMethod = "POST" request.setValue("application/x-www-form-urlencoded; charset=utf-8", forHTTPHeaderField: "Content-Type") request.setValue("no-store", forHTTPHeaderField: "Cache-Control") request.httpBody = Self.formEncodedBody([ URLQueryItem(name: "code", value: code), URLQueryItem(name: "code_verifier", value: verifier) ]) currentEnv = environment UserDefaults.standard.set(environment.rawValue, forKey: "SHDSelectedEnvironment") webView.load(request) } private func presentDesktopAuthError(_ message: String) { let alert = NSAlert() alert.messageText = "Вход в SHD" alert.informativeText = message alert.alertStyle = .warning alert.addButton(withTitle: "OK") alert.runModal() } func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { window ?? NSApp.keyWindow ?? NSWindow() } private static func randomBase64URLString(byteCount: Int) throws -> String { var bytes = [UInt8](repeating: 0, count: byteCount) let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) guard status == errSecSuccess else { throw DesktopAuthError.randomNumberGenerationFailed } return base64URLString(for: Data(bytes)) } private static func base64URLString(for data: Data) -> String { data.base64EncodedString() .replacingOccurrences(of: "+", with: "-") .replacingOccurrences(of: "/", with: "_") .replacingOccurrences(of: "=", with: "") } private static func formEncodedBody(_ items: [URLQueryItem]) -> Data { var components = URLComponents() components.queryItems = items return Data((components.percentEncodedQuery ?? "").utf8) } private enum DesktopAuthError: Error { case randomNumberGenerationFailed } func reload() { webView.reload() } func reloadIgnoringCache() { webView.reloadFromOrigin() } func goBack() { if webView.canGoBack { webView.goBack() } } func goForward() { if webView.canGoForward { webView.goForward() } } func zoomIn() { webView.pageZoom += 0.1 } func zoomOut() { if webView.pageZoom > 0.4 { webView.pageZoom -= 0.1 } } func resetZoom() { webView.pageZoom = 1.0 } func showInspector() { if let inspector = webView.value(forKey: "_inspector") as? NSObject { inspector.perform(Selector(("show"))) } } func showConsole() { if let inspector = webView.value(forKey: "_inspector") as? NSObject { inspector.perform(Selector(("showConsole"))) } } func printCurrentPage() { guard let window = window else { return } let printInfo = NSPrintInfo.shared.copy() as? NSPrintInfo ?? NSPrintInfo.shared printInfo.horizontalPagination = .fit printInfo.verticalPagination = .automatic printInfo.isHorizontallyCentered = true printInfo.isVerticallyCentered = false let printOp = webView.printOperation(with: printInfo) printOp.view?.frame = webView.bounds printOp.showsPrintPanel = true printOp.showsProgressPanel = true printOp.runModal(for: window, delegate: nil, didRun: nil, contextInfo: nil) } private func cleanTitle(from raw: String) -> String { // Strip out repetitive brand suffixes for clean, elegant tab titles var title = raw if let range = title.range(of: " — SHD Project") { title = String(title[.. (WKNavigationActionPolicy, WKWebpagePreferences) { if navigationAction.shouldPerformDownload { return (.download, preferences) } guard let url = navigationAction.request.url else { return (.allow, preferences) } let host = url.host?.lowercased() ?? "" let isInternalHost = host == "shd.xyz.su" || host == "dev-shd.xyz.su" || host == "sso.xyz.su" || host == "git.xyz.su" || host.hasSuffix(".xyz.su") if navigationAction.navigationType == .linkActivated { let openInNewTab = navigationAction.modifierFlags.contains(.command) || navigationAction.buttonNumber == 2 if isInternalHost { if openInNewTab { AppDelegate.shared?.openTab(with: navigationAction.request, relativeTo: self.window) return (.cancel, preferences) } return (.allow, preferences) } else { NSWorkspace.shared.open(url) return (.cancel, preferences) } } if isInternalHost { return (.allow, preferences) } return (.allow, preferences) } func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy { if navigationResponse.canShowMIMEType { return .allow } return .download } // MARK: - WKDownloadDelegate func webView(_ webView: WKWebView, navigationAction: WKNavigationAction, didBecome download: WKDownload) { download.delegate = self } func webView(_ webView: WKWebView, navigationResponse: WKNavigationResponse, didBecome download: WKDownload) { download.delegate = self } func download(_ download: WKDownload, decideDestinationUsing response: URLResponse, suggestedFilename: String) async -> URL? { let downloadsDir = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first ?? URL(fileURLWithPath: NSHomeDirectory()) var destination = downloadsDir.appendingPathComponent(suggestedFilename) var counter = 1 let baseName = (suggestedFilename as NSString).deletingPathExtension let ext = (suggestedFilename as NSString).pathExtension while FileManager.default.fileExists(atPath: destination.path) { let newName = ext.isEmpty ? "\(baseName) (\(counter))" : "\(baseName) (\(counter)).\(ext)" destination = downloadsDir.appendingPathComponent(newName) counter += 1 } return destination } func downloadDidFinish(_ download: WKDownload) { NSSound(named: "Glass")?.play() } // MARK: - WKUIDelegate (Native Alerts, Confirms, Prompts, Media Permissions & File Uploads) func webView(_ webView: WKWebView, decideMediaCapturePermissionsFor origin: WKSecurityOrigin, initiatedBy frame: WKFrameInfo, type: WKMediaCaptureType) async -> WKPermissionDecision { let host = origin.host.lowercased() if host == "shd.xyz.su" || host == "dev-shd.xyz.su" || host.hasSuffix(".xyz.su") { return .grant } return .prompt } func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { if navigationAction.targetFrame == nil { if let url = navigationAction.request.url { let host = url.host?.lowercased() ?? "" if host == "shd.xyz.su" || host == "dev-shd.xyz.su" || host == "sso.xyz.su" || host == "git.xyz.su" || host.hasSuffix(".xyz.su") { AppDelegate.shared?.openTab(with: navigationAction.request, relativeTo: self.window) } else { NSWorkspace.shared.open(url) } } } return nil } func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo) async { let alert = NSAlert() alert.messageText = "SHD" alert.informativeText = message alert.alertStyle = .informational alert.addButton(withTitle: "OK") if let window = window { _ = await alert.beginSheetModal(for: window) } else { alert.runModal() } } func webView(_ webView: WKWebView, runJavaScriptConfirmPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo) async -> Bool { let alert = NSAlert() alert.messageText = "SHD" alert.informativeText = message alert.alertStyle = .warning alert.addButton(withTitle: "Подтвердить") alert.addButton(withTitle: "Отмена") let response: NSApplication.ModalResponse if let window = window { response = await alert.beginSheetModal(for: window) } else { response = alert.runModal() } return response == .alertFirstButtonReturn } func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo) async -> String? { let alert = NSAlert() alert.messageText = "SHD" alert.informativeText = prompt alert.addButton(withTitle: "OK") alert.addButton(withTitle: "Отмена") let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 260, height: 24)) input.stringValue = defaultText ?? "" alert.accessoryView = input let response: NSApplication.ModalResponse if let window = window { response = await alert.beginSheetModal(for: window) } else { response = alert.runModal() } if response == .alertFirstButtonReturn { return input.stringValue } return nil } func webView(_ webView: WKWebView, runOpenPanelWith parameters: WKOpenPanelParameters, initiatedByFrame frame: WKFrameInfo) async -> [URL]? { let panel = NSOpenPanel() panel.allowsMultipleSelection = parameters.allowsMultipleSelection panel.canChooseDirectories = parameters.allowsDirectories panel.canChooseFiles = true let response: NSApplication.ModalResponse if let window = window { response = await panel.beginSheetModal(for: window) } else { response = panel.runModal() } if response == .OK { return panel.urls } return nil } }