import Foundation import UIKit import Display import SwiftSignalKit import Postbox import TelegramCore #if BUCK import MtProtoKit #else import MtProtoKitDynamic #endif import TelegramPresentationData import ItemListUI import AccountContext import UrlEscaping import UrlHandling private func shareLink(for server: ProxyServerSettings) -> String { var link: String switch server.connection { case let .mtp(secret): let secret = MTProxySecret.parseData(secret)?.serializeToString() ?? "" link = "https://t.me/proxy?server=\(server.host)&port=\(server.port)" link += "&secret=\(secret.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" case let .socks5(username, password): link = "https://t.me/socks?server=\(server.host)&port=\(server.port)" link += "&user=\(username?.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")&pass=\(password?.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryValueAllowed) ?? "")" } return link } private final class proxyServerSettingsControllerArguments { let updateState: ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void let share: () -> Void let usePasteboardSettings: () -> Void init(updateState: @escaping ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void, share: @escaping () -> Void, usePasteboardSettings: @escaping () -> Void) { self.updateState = updateState self.share = share self.usePasteboardSettings = usePasteboardSettings } } private enum ProxySettingsSection: Int32 { case pasteboard case mode case connection case credentials case share } private enum ProxySettingsEntry: ItemListNodeEntry { case usePasteboardSettings(PresentationTheme, String) case usePasteboardInfo(PresentationTheme, String) case modeSocks5(PresentationTheme, String, Bool) case modeMtp(PresentationTheme, String, Bool) case connectionHeader(PresentationTheme, String) case connectionServer(PresentationTheme, PresentationStrings, String, String) case connectionPort(PresentationTheme, PresentationStrings, String, String) case credentialsHeader(PresentationTheme, String) case credentialsUsername(PresentationTheme, PresentationStrings, String, String) case credentialsPassword(PresentationTheme, PresentationStrings, String, String) case credentialsSecret(PresentationTheme, PresentationStrings, String, String) case share(PresentationTheme, String, Bool) var section: ItemListSectionId { switch self { case .usePasteboardSettings, .usePasteboardInfo: return ProxySettingsSection.pasteboard.rawValue case .modeSocks5, .modeMtp: return ProxySettingsSection.mode.rawValue case .connectionHeader, .connectionServer, .connectionPort: return ProxySettingsSection.connection.rawValue case .credentialsHeader, .credentialsUsername, .credentialsPassword, .credentialsSecret: return ProxySettingsSection.credentials.rawValue case .share: return ProxySettingsSection.share.rawValue } } var stableId: Int32 { switch self { case .usePasteboardSettings: return 0 case .usePasteboardInfo: return 1 case .modeSocks5: return 2 case .modeMtp: return 3 case .connectionHeader: return 4 case .connectionServer: return 5 case .connectionPort: return 6 case .credentialsHeader: return 7 case .credentialsUsername: return 8 case .credentialsPassword: return 9 case .credentialsSecret: return 10 case .share: return 12 } } static func <(lhs: ProxySettingsEntry, rhs: ProxySettingsEntry) -> Bool { return lhs.stableId < rhs.stableId } func item(_ arguments: proxyServerSettingsControllerArguments) -> ListViewItem { switch self { case let .usePasteboardSettings(theme, title): return ItemListActionItem(theme: theme, title: title, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.usePasteboardSettings() }) case let .usePasteboardInfo(theme, text): return ItemListTextItem(theme: theme, text: .plain(text), sectionId: self.section) case let .modeSocks5(theme, text, value): return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateState { state in var state = state state.mode = .socks5 return state } }) case let .modeMtp(theme, text, value): return ItemListCheckboxItem(theme: theme, title: text, style: .left, checked: value, zeroSeparatorInsets: false, sectionId: self.section, action: { arguments.updateState { state in var state = state state.mode = .mtp return state } }) case let .connectionHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .connectionServer(theme, strings, placeholder, text): return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.host = value return state } }, action: {}) case let .connectionPort(theme, strings, placeholder, text): return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: text, placeholder: placeholder, type: .number, sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.port = value return state } }, action: {}) case let .credentialsHeader(theme, text): return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) case let .credentialsUsername(theme, strings, placeholder, text): return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: text, placeholder: placeholder, sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.username = value return state } }, action: {}) case let .credentialsPassword(theme, strings, placeholder, text): return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: text, placeholder: placeholder, type: .password, sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.password = value return state } }, action: {}) case let .credentialsSecret(theme, strings, placeholder, text): return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: false), sectionId: self.section, textUpdated: { value in arguments.updateState { current in var state = current state.secret = value return state } }, action: {}) case let .share(theme, text, enabled): return ItemListActionItem(theme: theme, title: text, kind: enabled ? .generic : .disabled, alignment: .natural, sectionId: self.section, style: .blocks, action: { arguments.share() }) } } } private enum ProxyServerSettingsControllerMode { case socks5 case mtp } private struct ProxyServerSettingsControllerState: Equatable { var mode: ProxyServerSettingsControllerMode var host: String var port: String var username: String var password: String var secret: String var isComplete: Bool { if self.host.isEmpty || self.port.isEmpty || Int(self.port) == nil { return false } switch self.mode { case .socks5: break case .mtp: let secretIsValid = MTProxySecret.parse(self.secret) != nil if !secretIsValid { return false } } return true } } private func proxyServerSettingsControllerEntries(presentationData: (theme: PresentationTheme, strings: PresentationStrings), state: ProxyServerSettingsControllerState, pasteboardSettings: ProxyServerSettings?) -> [ProxySettingsEntry] { var entries: [ProxySettingsEntry] = [] if let _ = pasteboardSettings { entries.append(.usePasteboardSettings(presentationData.theme, presentationData.strings.SocksProxySetup_PasteFromClipboard)) } entries.append(.modeSocks5(presentationData.theme, presentationData.strings.SocksProxySetup_ProxySocks5, state.mode == .socks5)) entries.append(.modeMtp(presentationData.theme, presentationData.strings.SocksProxySetup_ProxyTelegram, state.mode == .mtp)) entries.append(.connectionHeader(presentationData.theme, presentationData.strings.SocksProxySetup_Connection.uppercased())) entries.append(.connectionServer(presentationData.theme, presentationData.strings, presentationData.strings.SocksProxySetup_Hostname, state.host)) entries.append(.connectionPort(presentationData.theme, presentationData.strings, presentationData.strings.SocksProxySetup_Port, state.port)) switch state.mode { case .socks5: entries.append(.credentialsHeader(presentationData.theme, presentationData.strings.SocksProxySetup_Credentials)) entries.append(.credentialsUsername(presentationData.theme, presentationData.strings, presentationData.strings.SocksProxySetup_Username, state.username)) entries.append(.credentialsPassword(presentationData.theme, presentationData.strings, presentationData.strings.SocksProxySetup_Password, state.password)) case .mtp: entries.append(.credentialsHeader(presentationData.theme, presentationData.strings.SocksProxySetup_RequiredCredentials)) entries.append(.credentialsSecret(presentationData.theme, presentationData.strings, presentationData.strings.SocksProxySetup_SecretPlaceholder, state.secret)) } entries.append(.share(presentationData.theme, presentationData.strings.Conversation_ContextMenuShare, state.isComplete)) return entries } private func proxyServerSettings(with state: ProxyServerSettingsControllerState) -> ProxyServerSettings? { if state.isComplete, let port = Int32(state.port) { switch state.mode { case .socks5: return ProxyServerSettings(host: state.host, port: port, connection: .socks5(username: state.username.isEmpty ? nil : state.username, password: state.password.isEmpty ? nil : state.password)) case .mtp: let parsedSecret = MTProxySecret.parse(state.secret) if let parsedSecret = parsedSecret { return ProxyServerSettings(host: state.host, port: port, connection: .mtp(secret: parsedSecret.serialize())) } } } return nil } public func proxyServerSettingsController(context: AccountContext, currentSettings: ProxyServerSettings? = nil) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with { $0 } return proxyServerSettingsController(theme: presentationData.theme, strings: presentationData.strings, updatedPresentationData: context.sharedContext.presentationData |> map { ($0.theme, $0.strings) }, accountManager: context.sharedContext.accountManager, postbox: context.account.postbox, network: context.account.network, currentSettings: currentSettings) } func proxyServerSettingsController(theme: PresentationTheme, strings: PresentationStrings, updatedPresentationData: Signal<(theme: PresentationTheme, strings: PresentationStrings), NoError>, accountManager: AccountManager, postbox: Postbox, network: Network, currentSettings: ProxyServerSettings?) -> ViewController { var currentMode: ProxyServerSettingsControllerMode = .socks5 var currentUsername: String? var currentPassword: String? var currentSecret: String? var pasteboardSettings: ProxyServerSettings? if let currentSettings = currentSettings { switch currentSettings.connection { case let .socks5(username, password): currentUsername = username currentPassword = password currentMode = .socks5 case let .mtp(secret): currentSecret = hexString(secret) currentMode = .mtp } } else { if let proxy = parseProxyUrl(UIPasteboard.general.string ?? "") { if let secret = proxy.secret, let parsedSecret = MTProxySecret.parseData(secret) { pasteboardSettings = ProxyServerSettings(host: proxy.host, port: proxy.port, connection: .mtp(secret: parsedSecret.serialize())) } else { pasteboardSettings = ProxyServerSettings(host: proxy.host, port: proxy.port, connection: .socks5(username: proxy.username, password: proxy.password)) } } } let initialState = ProxyServerSettingsControllerState(mode: currentMode, host: currentSettings?.host ?? "", port: (currentSettings?.port).flatMap { "\($0)" } ?? "", username: currentUsername ?? "", password: currentPassword ?? "", secret: currentSecret ?? "") let stateValue = Atomic(value: initialState) let statePromise = ValuePromise(initialState, ignoreRepeated: true) let updateState: ((ProxyServerSettingsControllerState) -> ProxyServerSettingsControllerState) -> Void = { f in statePromise.set(stateValue.modify { f($0) }) } var presentImpl: ((ViewController, Any?) -> Void)? var dismissImpl: (() -> Void)? var shareImpl: (() -> Void)? let arguments = proxyServerSettingsControllerArguments(updateState: { f in updateState(f) }, share: { shareImpl?() }, usePasteboardSettings: { if let pasteboardSettings = pasteboardSettings { updateState { state in var state = state state.host = pasteboardSettings.host state.port = "\(pasteboardSettings.port)" switch pasteboardSettings.connection { case let .socks5(username, password): state.mode = .socks5 state.username = username ?? "" state.password = password ?? "" case let .mtp(secret): state.mode = .mtp state.secret = hexString(secret) } return state } } }) let signal = combineLatest(updatedPresentationData, statePromise.get()) |> deliverOnMainQueue |> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState, ProxySettingsEntry.ItemGenerationArguments)) in let leftNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: { dismissImpl?() }) let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .bold, enabled: state.isComplete, action: { if let proxyServerSettings = proxyServerSettings(with: state) { let _ = (updateProxySettingsInteractively(accountManager: accountManager, { settings in var settings = settings if let currentSettings = currentSettings { if let index = settings.servers.firstIndex(of: currentSettings) { settings.servers[index] = proxyServerSettings if settings.activeServer == currentSettings { settings.activeServer = proxyServerSettings } } } else { settings.servers.append(proxyServerSettings) if settings.servers.count == 1 { settings.activeServer = proxyServerSettings } } return settings }) |> deliverOnMainQueue).start(completed: { dismissImpl?() }) } }) let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.SocksProxySetup_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let listState = ItemListNodeState(entries: proxyServerSettingsControllerEntries(presentationData: presentationData, state: state, pasteboardSettings: pasteboardSettings), style: .blocks, emptyStateItem: nil, animateChanges: false) return (controllerState, (listState, arguments)) } let controller = ItemListController(theme: theme, strings: strings, updatedPresentationData: updatedPresentationData, state: signal, tabBarItem: nil) controller.navigationPresentation = .modal presentImpl = { [weak controller] c, d in controller?.present(c, in: .window(.root), with: d) } dismissImpl = { [weak controller] in let _ = controller?.dismiss() } shareImpl = { [weak controller] in let state = stateValue.with { $0 } guard let server = proxyServerSettings(with: state), let strongController = controller else { return } let link = shareLink(for: server) controller?.view.endEditing(true) if #available(iOSApplicationExtension 9.0, iOS 9.0, *) { let controller = ShareProxyServerActionSheetController(theme: theme, strings: strings, updatedPresentationData: updatedPresentationData, link: link) presentImpl?(controller, nil) } else { let activityController = UIActivityViewController(activityItems: [link], applicationActivities: nil) if let window = strongController.view.window, let rootViewController = window.rootViewController { activityController.popoverPresentationController?.sourceView = window activityController.popoverPresentationController?.sourceRect = CGRect(origin: CGPoint(x: window.bounds.width / 2.0, y: window.bounds.size.height - 1.0), size: CGSize(width: 1.0, height: 1.0)) rootViewController.present(activityController, animated: true, completion: nil) } } } return controller }