From 00f5cebdef1bc253166669178c737fdc929b4340 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Wed, 27 May 2020 16:48:29 +1000 Subject: [PATCH] Implement basic onion request UI --- Signal.xcodeproj/project.pbxproj | 8 + .../src/Loki/Components/PathStatusView.swift | 45 ++++++ Signal/src/Loki/View Controllers/HomeVC.swift | 18 +++ Signal/src/Loki/View Controllers/PathVC.swift | 140 ++++++++++++++++++ .../translations/en.lproj/Localizable.strings | 2 + .../Loki/Redesign/Style Guide/Values.swift | 4 + .../src/Loki/API/LokiAPITarget.swift | 12 +- .../API/Onion Requests/OnionRequestAPI.swift | 16 +- 8 files changed, 231 insertions(+), 14 deletions(-) create mode 100644 Signal/src/Loki/Components/PathStatusView.swift create mode 100644 Signal/src/Loki/View Controllers/PathVC.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 8abe2ee6d..4eaf6a13a 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -592,6 +592,8 @@ B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.swift */; }; B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08523399CEF000F5AE3 /* SeedModal.swift */; }; B8783E9E23EB948D00404FB8 /* UILabel+Interaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */; }; + B879D449247E1BE300DB3608 /* PathVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B879D448247E1BE300DB3608 /* PathVC.swift */; }; + B879D44B247E1D9200DB3608 /* PathStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B879D44A247E1D9200DB3608 /* PathStatusView.swift */; }; B885D5F4233491AB00EE0D8E /* DeviceLinkingModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B885D5F3233491AB00EE0D8E /* DeviceLinkingModal.swift */; }; B886B4A72398B23E00211ABE /* QRCodeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B886B4A62398B23E00211ABE /* QRCodeVC.swift */; }; B886B4A92398BA1500211ABE /* QRCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B886B4A82398BA1500211ABE /* QRCode.swift */; }; @@ -1462,6 +1464,8 @@ B86BD08323399ACF000F5AE3 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = ""; }; B86BD08523399CEF000F5AE3 /* SeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedModal.swift; sourceTree = ""; }; B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Interaction.swift"; sourceTree = ""; }; + B879D448247E1BE300DB3608 /* PathVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathVC.swift; sourceTree = ""; }; + B879D44A247E1D9200DB3608 /* PathStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathStatusView.swift; sourceTree = ""; }; B885D5F3233491AB00EE0D8E /* DeviceLinkingModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLinkingModal.swift; sourceTree = ""; }; B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = ""; }; B886B4A62398B23E00211ABE /* QRCodeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeVC.swift; sourceTree = ""; }; @@ -2871,6 +2875,7 @@ B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */, B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */, B83F2B85240C7B8F000A54AB /* NewConversationButtonSet.swift */, + B879D44A247E1D9200DB3608 /* PathStatusView.swift */, C353F8F8244809150011121A /* PNOptionView.swift */, B8BB82B02390C37000BA5194 /* SearchBar.swift */, B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */, @@ -2917,6 +2922,7 @@ B8CCF63623961D6D0091D419 /* NewPrivateChatVC.swift */, B894D0742339EDCF00B4D94D /* NukeDataModal.swift */, C3DFFAC723E970080058DAF8 /* OpenGroupSuggestionSheet.swift */, + B879D448247E1BE300DB3608 /* PathVC.swift */, C353F8F6244808E90011121A /* PNModeSheet.swift */, C3548F0524456447009433A8 /* PNModeVC.swift */, B886B4A62398B23E00211ABE /* QRCodeVC.swift */, @@ -4010,6 +4016,7 @@ 341F2C0F1F2B8AE700D07D6B /* DebugUIMisc.m in Sources */, B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */, B83F2B86240C7B8F000A54AB /* NewConversationButtonSet.swift in Sources */, + B879D449247E1BE300DB3608 /* PathVC.swift in Sources */, 340FC8AF204DAC8D007AEB0F /* OWSLinkDeviceViewController.m in Sources */, 34E3EF0D1EFC235B007F6822 /* DebugUIDiskUsage.m in Sources */, 454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */, @@ -4146,6 +4153,7 @@ 34D8C0281ED3673300188D7C /* DebugUITableViewController.m in Sources */, 45F32C222057297A00A300D5 /* MediaDetailViewController.m in Sources */, B8B26C8F234D629C004ED98C /* MentionCandidateSelectionView.swift in Sources */, + B879D44B247E1D9200DB3608 /* PathStatusView.swift in Sources */, 34B3F8851E8DF1700035BE1A /* NewGroupViewController.m in Sources */, B8CCF63F23975CFB0091D419 /* JoinPublicChatVC.swift in Sources */, 34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */, diff --git a/Signal/src/Loki/Components/PathStatusView.swift b/Signal/src/Loki/Components/PathStatusView.swift new file mode 100644 index 000000000..9499864b1 --- /dev/null +++ b/Signal/src/Loki/Components/PathStatusView.swift @@ -0,0 +1,45 @@ + +final class PathStatusView : UIView { + + override init(frame: CGRect) { + super.init(frame: frame) + setUpViewHierarchy() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setUpViewHierarchy() + } + + private func setUpViewHierarchy() { + backgroundColor = Colors.accent + let size = Values.pathStatusViewSize + layer.cornerRadius = size / 2 + setGlow(to: size, with: Colors.accent, animated: false) + layer.masksToBounds = false + } + + func setGlow(to size: CGFloat, with color: UIColor, animated isAnimated: Bool) { + let newPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: 0, y: 0), size: CGSize(width: size, height: size))).cgPath + if isAnimated { + let pathAnimation = CABasicAnimation(keyPath: "shadowPath") + pathAnimation.fromValue = layer.shadowPath + pathAnimation.toValue = newPath + pathAnimation.duration = 0.25 + layer.add(pathAnimation, forKey: pathAnimation.keyPath) + } + layer.shadowPath = newPath + let newColor = color.cgColor + if isAnimated { + let colorAnimation = CABasicAnimation(keyPath: "shadowColor") + colorAnimation.fromValue = layer.shadowColor + colorAnimation.toValue = newColor + colorAnimation.duration = 0.25 + layer.add(colorAnimation, forKey: colorAnimation.keyPath) + } + layer.shadowColor = newColor + layer.shadowOffset = CGSize(width: 0, height: 0.8) + layer.shadowOpacity = isLightMode ? 0.4 : 1 + layer.shadowRadius = isLightMode ? 6 : 8 + } +} diff --git a/Signal/src/Loki/View Controllers/HomeVC.swift b/Signal/src/Loki/View Controllers/HomeVC.swift index c9690c678..8a80556eb 100644 --- a/Signal/src/Loki/View Controllers/HomeVC.swift +++ b/Signal/src/Loki/View Controllers/HomeVC.swift @@ -299,6 +299,18 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol profilePictureView.pin(.trailing, to: .trailing, of: profilePictureViewContainer) profilePictureView.pin(.bottom, to: .bottom, of: profilePictureViewContainer) navigationItem.leftBarButtonItem = UIBarButtonItem(customView: profilePictureViewContainer) + let pathStatusViewContainer = UIView() + let pathStatusViewContainerSize = Values.verySmallProfilePictureSize // Match the profile picture view + pathStatusViewContainer.set(.width, to: pathStatusViewContainerSize) + pathStatusViewContainer.set(.height, to: pathStatusViewContainerSize) + let pathStatusView = PathStatusView() + pathStatusView.set(.width, to: Values.pathStatusViewSize) + pathStatusView.set(.height, to: Values.pathStatusViewSize) + pathStatusViewContainer.addSubview(pathStatusView) + pathStatusView.center(.horizontal, in: pathStatusViewContainer) + pathStatusView.center(.vertical, in: pathStatusViewContainer) + pathStatusViewContainer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showPath))) + navigationItem.rightBarButtonItem = UIBarButtonItem(customView: pathStatusViewContainer) } // MARK: Interaction @@ -398,6 +410,12 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, UIScrol present(navigationController, animated: true, completion: nil) } + @objc private func showPath() { + let pathVC = PathVC() + let navigationController = OWSNavigationController(rootViewController: pathVC) + present(navigationController, animated: true, completion: nil) + } + @objc func joinOpenGroup() { let joinPublicChatVC = JoinPublicChatVC() let navigationController = OWSNavigationController(rootViewController: joinPublicChatVC) diff --git a/Signal/src/Loki/View Controllers/PathVC.swift b/Signal/src/Loki/View Controllers/PathVC.swift new file mode 100644 index 000000000..16c3fc924 --- /dev/null +++ b/Signal/src/Loki/View Controllers/PathVC.swift @@ -0,0 +1,140 @@ + +final class PathVC : BaseVC { + + // MARK: Lifecycle + override func viewDidLoad() { + super.viewDidLoad() + // Set gradient background + view.backgroundColor = .clear + let gradient = Gradients.defaultLokiBackground + view.setGradient(gradient) + // Set up navigation bar + let navigationBar = navigationController!.navigationBar + navigationBar.setBackgroundImage(UIImage(), for: UIBarMetrics.default) + navigationBar.shadowImage = UIImage() + navigationBar.isTranslucent = false + navigationBar.barTintColor = Colors.navigationBarBackground + // Set up close button + let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close)) + closeButton.tintColor = Colors.text + navigationItem.leftBarButtonItem = closeButton + // Customize title + let titleLabel = UILabel() + titleLabel.text = NSLocalizedString("Path", comment: "") + titleLabel.textColor = Colors.text + titleLabel.font = .boldSystemFont(ofSize: Values.veryLargeFontSize) + navigationItem.titleView = titleLabel + // Set up explanation label + let explanationLabel = UILabel() + explanationLabel.textColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity) + explanationLabel.font = .systemFont(ofSize: Values.smallFontSize) + explanationLabel.text = NSLocalizedString("Session hides your IP by onion routing your messages through Session's decentralized Service Node network. The Service Nodes currently being used for this are shown below.", comment: "") + explanationLabel.numberOfLines = 0 + explanationLabel.textAlignment = .center + explanationLabel.lineBreakMode = .byWordWrapping + view.addSubview(explanationLabel) + explanationLabel.pin(.leading, to: .leading, of: view, withInset: Values.largeSpacing) + explanationLabel.pin(.top, to: .top, of: view, withInset: Values.mediumSpacing) + explanationLabel.pin(.trailing, to: .trailing, of: view, withInset: -Values.largeSpacing) + // Set up path stack view + guard var mainPath = OnionRequestAPI.paths.first else { + return close() // TODO: Show path establishing UI + } + let rows: [UIStackView] + switch mainPath.count { + case 1: return // TODO: Do we want to handle this case? + case 2: + let topPathRow = getPathRow(forSnode: mainPath[1], at: .top) + let bottomPathRow = getPathRow(forSnode: mainPath[0], at: .bottom) + rows = [ topPathRow, bottomPathRow ] + default: + let topPathRow = getPathRow(forSnode: mainPath.removeLast(), at: .top) + let bottomPathRow = getPathRow(forSnode: mainPath.removeFirst(), at: .bottom) + let middlePathRows = mainPath.map { + getPathRow(forSnode: $0, at: .middle) + } + rows = [ topPathRow ] + middlePathRows + [ bottomPathRow ] + } + let pathStackView = UIStackView(arrangedSubviews: rows) + pathStackView.axis = .vertical + view.addSubview(pathStackView) + pathStackView.pin(.top, to: .bottom, of: explanationLabel, withInset: Values.largeSpacing) + pathStackView.center(.horizontal, in: view) + pathStackView.leadingAnchor.constraint(greaterThanOrEqualTo: view.leadingAnchor, constant: Values.largeSpacing).isActive = true + view.trailingAnchor.constraint(greaterThanOrEqualTo: pathStackView.trailingAnchor, constant: Values.largeSpacing).isActive = true + } + + private func getPathRow(forSnode snode: LokiAPITarget, at location: LineView.Location) -> UIStackView { + let lineView = LineView(location: location) + lineView.set(.width, to: Values.pathRowDotSize) + let snodeLabel = UILabel() + snodeLabel.textColor = Colors.text + snodeLabel.font = .systemFont(ofSize: Values.mediumFontSize) + var snodeDescription = snode.description + if snodeDescription.hasPrefix("https://") { + snodeDescription.removeFirst(8) + } + if let colonIndex = snodeDescription.lastIndex(of: ":") { + snodeDescription = String(snodeDescription[snodeDescription.startIndex.. Bool { + override public func isEqual(_ other: Any?) -> Bool { guard let other = other as? LokiAPITarget else { return false } return address == other.address && port == other.port } // MARK: Hashing - override internal var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:) + override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:) return address.hashValue ^ port.hashValue } // MARK: Description - override internal var description: String { return "\(address):\(port)" } + override public var description: String { return "\(address):\(port)" } } diff --git a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift index fe03b9e9c..0e38df505 100644 --- a/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift +++ b/SignalServiceKit/src/Loki/API/Onion Requests/OnionRequestAPI.swift @@ -2,11 +2,11 @@ import CryptoSwift import PromiseKit /// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information. -internal enum OnionRequestAPI { +public enum OnionRequestAPI { /// - Note: Must only be modified from `LokiAPI.workQueue`. private static var guardSnodes: Set = [] /// - Note: Must only be modified from `LokiAPI.workQueue`. - private static var paths: Set = [] + public static var paths: [Path] = [] private static var snodePool: Set { let unreliableSnodes = Set(LokiAPI.failureCount.keys) @@ -42,7 +42,7 @@ internal enum OnionRequestAPI { } // MARK: Path - internal typealias Path = [LokiAPITarget] + public typealias Path = [LokiAPITarget] // MARK: Onion Building Result private typealias OnionBuildingResult = (guardSnode: LokiAPITarget, finalEncryptionResult: EncryptionResult, targetSnodeSymmetricKey: Data) @@ -54,7 +54,7 @@ internal enum OnionRequestAPI { let queue = DispatchQueue.global() // No need to block the work queue for this queue.async { let url = "\(snode.address):\(snode.port)/get_stats/v1" - let timeout: TimeInterval = 6 // Use a shorter timeout for testing + let timeout: TimeInterval = 3 // Use a shorter timeout for testing HTTP.execute(.get, url, timeout: timeout).done(on: queue) { rawResponse in guard let json = rawResponse as? JSON, let version = json["version"] as? String else { return seal.reject(Error.missingSnodeVersion) } if version >= "2.0.0" { @@ -100,15 +100,15 @@ internal enum OnionRequestAPI { /// Builds and returns `pathCount` paths. The returned promise errors out with `Error.insufficientSnodes` /// if not enough (reliable) snodes are available. - private static func buildPaths() -> Promise> { + private static func buildPaths() -> Promise<[Path]> { print("[Loki] [Onion Request API] Building onion request paths.") - return LokiAPI.getRandomSnode().then(on: LokiAPI.workQueue) { _ -> Promise> in // Just used to populate the snode pool + return LokiAPI.getRandomSnode().then(on: LokiAPI.workQueue) { _ -> Promise<[Path]> in // Just used to populate the snode pool return getGuardSnodes().map(on: LokiAPI.workQueue) { guardSnodes in var unusedSnodes = snodePool.subtracting(guardSnodes) let pathSnodeCount = guardSnodeCount * pathSize - guardSnodeCount guard unusedSnodes.count >= pathSnodeCount else { throw Error.insufficientSnodes } // Don't test path snodes as this would reveal the user's IP to them - return Set(guardSnodes.map { guardSnode in + return guardSnodes.map { guardSnode in let result = [ guardSnode ] + (0..<(pathSize - 1)).map { _ in // randomElement() uses the system's default random generator, which is cryptographically secure let pathSnode = unusedSnodes.randomElement()! // Safe because of the pathSnodeCount check above @@ -117,7 +117,7 @@ internal enum OnionRequestAPI { } print("[Loki] [Onion Request API] Built new onion request path: \(result.prettifiedDescription).") return result - }) + } } } }