// _____ _ // |_ _| |_ _ _ ___ ___ _ __ __ _ // | | | ' \| '_/ -_) -_) ' \/ _` |_ // |_| |_||_|_| \___\___|_|_|_\__,_(_) // // Threema iOS Client // Copyright (c) 2018-2020 Threema GmbH // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License, version 3, // as published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import UIKit import ThreemaFramework class ThreemaWebViewController: ThemedTableViewController { @IBOutlet weak var cameraButton: UIBarButtonItem! var entityManager: EntityManager = EntityManager() var fetchedResultsController: NSFetchedResultsController? var selectedIndexPath: IndexPath? override func viewDidLoad() { super.viewDidLoad() fetchedResultsController = entityManager.entityFetcher.fetchedResultsControllerForWebClientSessions() fetchedResultsController!.delegate = self do { try fetchedResultsController!.performFetch() } catch { ErrorHandler.abortWithError(nil) } self.title = NSLocalizedString("webClientSession_title", comment: "") } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) let mdmSetup = MDMSetup(setup: false)! cameraButton.isEnabled = !mdmSetup.disableWeb() cleanupWebClientSessions() cameraButton.image = BundleUtil.imageNamed("QRScan").withTint(Colors.main()) cameraButton.accessibilityLabel = BundleUtil.localizedString(forKey: "scan_qr") tableView.reloadData() } // MARK: - Private functions private func presentActionSheetForSession(_ webClientSession: WebClientSession, indexPath: IndexPath) { let alert = UIAlertController(title: NSLocalizedString("webClientSession_actionSheetTitle", comment: ""), message: nil, preferredStyle: .actionSheet) if (webClientSession.active?.boolValue)! { // Stop action alert.addAction(UIAlertAction(title: NSLocalizedString("webClientSession_actionSheet_stopSession", comment: ""), style: .default) { stopAction in ValidationLogger.shared().logString("Threema Web: Disconnect webclient userStoppedSession") WCSessionManager.shared.stopSession(webClientSession) }) } else { // Start action alert.addAction(UIAlertAction(title: NSLocalizedString("webClientSession_actionSheet_startSession", comment: ""), style: .default) { stopAction in ValidationLogger.shared().logString("Threema Web: User start connection") WCSessionManager.shared.connect(authToken: nil, wca: nil, webClientSession: webClientSession) }) } // Rename action alert.addAction(UIAlertAction(title: NSLocalizedString("webClientSession_actionSheet_renameSession", comment: ""), style: .default) { _ in let renameAlert = UIAlertController(title: NSLocalizedString("webClientSession_sessionName", comment: ""), message: nil, preferredStyle: .alert) renameAlert.addTextField { textfield in if let sessionName = webClientSession.name { textfield.text = sessionName } else { textfield.placeholder = NSLocalizedString("webClientSession_unnamed", comment: "") } } let saveAction = UIAlertAction(title: NSLocalizedString("save", comment: ""), style: .default) { alertAction in let textField = renameAlert.textFields![0] WebClientSessionStore.shared.updateWebClientSession(session: webClientSession, sessionName: textField.text) self.tableView.deselectRow(at: indexPath, animated: true) } renameAlert.addAction(saveAction) let cancelAction = UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .cancel) { _ in self.tableView.deselectRow(at: indexPath, animated: true) } renameAlert.addAction(cancelAction) self.present(renameAlert, animated: true) }) // Delete action alert.addAction(UIAlertAction(title: NSLocalizedString("webClientSession_actionSheet_deleteSession", comment: ""), style: .destructive) { _ in self.deleteWebClientSession(webClientSession) }) // Cancel action alert.addAction(UIAlertAction(title: NSLocalizedString("cancel", comment: ""), style: .cancel) { _ in self.tableView.deselectRow(at: indexPath, animated: true) }) self.present(alert, animated: true) } /// Will delete all not persistent sessions where are older then 24 hours and not active private func cleanupWebClientSessions() { guard let allSessions = entityManager.entityFetcher.allWebClientSessions() as? [WebClientSession] else { return } for session in allSessions { if session.permanent?.boolValue == true { continue } if let date = session.lastConnection { if let diff = Calendar.current.dateComponents([.hour], from: date, to: Date()).hour, diff > 24 { if session.active?.boolValue == false { WebClientSessionStore.shared.deleteWebClientSession(session) } } } } } private func deleteWebClientSession(_ session: WebClientSession) { WCSessionManager.shared.stopAndDeleteSession(session) if fetchedResultsController!.fetchedObjects!.count == 0 { UserSettings.shared().threemaWeb = false } } // MARK: - Table view data source override func numberOfSections(in tableView: UITableView) -> Int { return fetchedResultsController!.sections!.count + 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if section == 0 { return 1 } return fetchedResultsController!.fetchedObjects!.count } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { if section == 1 { return NSLocalizedString("webClientSession_sessions_header", comment: "") } return nil } override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { if section == 0 { let mdmSetup = MDMSetup(setup: false)! if mdmSetup.existsMdmKey(MDM_KEY_DISABLE_WEB) { return NSLocalizedString("disabled_by_device_policy", comment: "") } else { return NSLocalizedString("webClientSession_add_footer", comment: "") } } return nil } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if indexPath.section == 0 { let cell = tableView.dequeueReusableCell(withIdentifier: "ThreemaWebSettingCell", for: indexPath) as! ThreemaWebSettingCell cell.setupCell() return cell } else { let cell = tableView.dequeueReusableCell(withIdentifier: "WebClientSessionCell", for: indexPath) as! WebClientSessionCell cell.webClientSession = fetchedResultsController!.fetchedObjects![indexPath.row] as? WebClientSession cell.viewController = self cell.setupCell() return cell } } override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { if indexPath.section == 0 && indexPath.row == 0 { return nil } return indexPath } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if indexPath.section == 1 { let webClientSession = fetchedResultsController!.fetchedObjects![indexPath.row] as! WebClientSession presentActionSheetForSession(webClientSession, indexPath: indexPath) } } override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return indexPath.section == 1 } override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { let webClientSession = fetchedResultsController!.fetchedObjects![indexPath.row] as! WebClientSession deleteWebClientSession(webClientSession) } } } extension ThreemaWebViewController { @IBAction func scanNewSession(_ sender: Any?) { let qrController = QRScannerViewController() qrController.delegate = self qrController.title = NSLocalizedString("scan_qr", comment: "") let nav = PortraitNavigationController(rootViewController: qrController) nav.navigationBar.barStyle = .blackTranslucent nav.navigationBar.tintColor = Colors.main() nav.modalTransitionStyle = .crossDissolve self.present(nav, animated: true) } @IBAction func threemaWebSwitchChanged(_ sender: Any) { let threemaWebSwitch = sender as! UISwitch UserSettings.shared().threemaWeb = threemaWebSwitch.isOn if !threemaWebSwitch.isOn { // disconnect if there is a active session ValidationLogger.shared()?.logString("Threema Web: Disconnect webclient threemaWebOff") WCSessionManager.shared.stopAllSessions() // delete all not saved sessions if it's off WCSessionManager.shared.removeAllNotPermanentSessions() } else { if fetchedResultsController!.fetchedObjects!.count == 0 { scanNewSession(nil) } } } } extension ThreemaWebViewController: QRScannerViewControllerDelegate { func qrScannerViewController(_ controller: QRScannerViewController!, didScanResult result: String!) { let data = Data(base64Encoded: result) if UserSettings.shared().inAppVibrate { AudioServicesPlayAlertSound(kSystemSoundID_Vibrate) } if UserSettings.shared().threemaWeb == false { UserSettings.shared().threemaWeb = true } guard let qrCodeData = data else { ValidationLogger.shared().logString("Threema Web: Can't read qr code") let alert = UIAlertController(title: NSLocalizedString("webClientSession_add_wrong_qr_title", comment: ""), message: NSLocalizedString("webClientSession_add_wrong_qr_message", comment: ""), preferredStyle: .alert) alert.addAction(UIAlertAction(title: NSLocalizedString("ok", comment: ""), style: .default)) self.dismiss(animated: true) { self.present(alert, animated: true) } return } func scannedCodeHandler() { handleScannedCode(data: qrCodeData) { error in if error == false { self.dismiss(animated: true, completion: nil) } else { controller.stopRunning() } } } // Don't enable Threema Web until all contacts have a non-nil feature mask let contacts = ContactStore.shared().contactsWithFeatureMaskNil() if let nilContacts = contacts, nilContacts.count > 0 { ContactStore.shared().updateFeatureMasks(forContacts: nilContacts, onCompletion: { scannedCodeHandler() }) { error in scannedCodeHandler() } } else { scannedCodeHandler() } } func qrScannerViewControllerDidCancel(_ controller: QRScannerViewController!) { self.dismiss(animated: true) } private func handleScannedCode(data: Data, completion: @escaping (_ error: Bool?) -> Void) { do { let format = String(format: ">HB32B32B32BH%is", (data.count) - 101) let a = try unpack(format, data) let allOptions = Bitfield(rawValue: a[1] as! Int) var initiatorPermanentPublicKeyArray = [UInt8]() for index in 2...33 { initiatorPermanentPublicKeyArray.append(UInt8(a[index] as! Int)) } var authTokenArray = [UInt8]() for index in 34...65 { authTokenArray.append(UInt8(a[index] as! Int)) } var serverPermanentPublicKeyArray = [UInt8]() for index in 66...97 { serverPermanentPublicKeyArray.append(UInt8(a[index] as! Int)) } let scanController = ScanIdentityController() scanController.playSuccessSound() var session = [String: Any]() session.updateValue(a[0] as! Int, forKey: "webClientVersion") session.updateValue(allOptions.contains(.permanent) ? true : false, forKey: "permanent") session.updateValue(allOptions.contains(.selfHosted) ? true : false, forKey: "selfHosted") session.updateValue(Data(initiatorPermanentPublicKeyArray), forKey: "initiatorPermanentPublicKey") session.updateValue(Data(serverPermanentPublicKeyArray), forKey: "serverPermanentPublicKey") session.updateValue((a[98] as! Int), forKey: "saltyRTCPort") session.updateValue(a[99] as! NSString, forKey: "saltyRTCHost") if LicenseStore.requiresLicenseKey() == true { let mdmSetup = MDMSetup(setup: false)! if let webHosts = mdmSetup.webHosts() { if WCSessionManager.isWebHostAllowed(scannedHostName: session["saltyRTCHost"] as! String, whiteList: webHosts) == false { ValidationLogger.shared().logString("Threema Web: Scanned qr code host is not white listed") let topViewController = AppDelegate.shared()?.currentTopViewController() UIAlertTemplate.showAlert(owner: topViewController!, title: BundleUtil.localizedString(forKey: "webClient_scan_error_mdm_host_title"), message: BundleUtil.localizedString(forKey: "webClient_scan_error_mdm_host_message")) { (action) in self.dismiss(animated: true, completion: nil) } completion(true) return } } } ValidationLogger.shared().logString("Threema Web: Scanned qr code") let webClientSession = WebClientSessionStore.shared.addWebClientSession(dictionary: session) WCSessionManager.shared.connect(authToken: Data(authTokenArray), wca: nil, webClientSession: webClientSession) completion(false) } catch { ValidationLogger.shared().logString("Threema Web: Can't read qr code") completion(false) } } } struct Bitfield: OptionSet { let rawValue: Int static let selfHosted = Bitfield(rawValue: 1 << 0) static let permanent = Bitfield(rawValue: 1 << 1) } extension ThreemaWebViewController: NSFetchedResultsControllerDelegate { public func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { tableView.reloadData() } @nonobjc public func controller(_ controller: NSFetchedResultsController, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) { tableView.reloadData() } public func controllerDidChangeContent(_ controller: NSFetchedResultsController) { tableView.reloadData() } }