Implement basic onion request UI

This commit is contained in:
Niels Andriesse 2020-05-27 16:48:29 +10:00
parent 8fb5e7102f
commit 00f5cebdef
8 changed files with 231 additions and 14 deletions

View File

@ -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 = "<group>"; };
B86BD08523399CEF000F5AE3 /* SeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedModal.swift; sourceTree = "<group>"; };
B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Interaction.swift"; sourceTree = "<group>"; };
B879D448247E1BE300DB3608 /* PathVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathVC.swift; sourceTree = "<group>"; };
B879D44A247E1D9200DB3608 /* PathStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathStatusView.swift; sourceTree = "<group>"; };
B885D5F3233491AB00EE0D8E /* DeviceLinkingModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceLinkingModal.swift; sourceTree = "<group>"; };
B885D5F52334A32100EE0D8E /* UIView+Constraints.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraints.swift"; sourceTree = "<group>"; };
B886B4A62398B23E00211ABE /* QRCodeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeVC.swift; sourceTree = "<group>"; };
@ -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 */,

View File

@ -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
}
}

View File

@ -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)

View File

@ -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..<colonIndex])
}
snodeLabel.text = snodeDescription
snodeLabel.lineBreakMode = .byTruncatingTail
let stackView = UIStackView(arrangedSubviews: [ lineView, snodeLabel ])
stackView.axis = .horizontal
stackView.spacing = Values.largeSpacing
return stackView
}
// MARK: Interaction
@objc private func close() {
dismiss(animated: true, completion: nil)
}
}
// MARK: Line View
private final class LineView : UIView {
private let location: Location
enum Location {
case top, middle, bottom
}
init(location: Location) {
self.location = location
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(location:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(location:) instead.")
}
private func setUpViewHierarchy() {
set(.height, to: Values.pathRowHeight)
let lineView = UIView()
lineView.set(.width, to: Values.pathRowLineThickness)
lineView.backgroundColor = Colors.text
addSubview(lineView)
lineView.center(.horizontal, in: self)
switch location {
case .top: lineView.topAnchor.constraint(equalTo: centerYAnchor).isActive = true
case .middle, .bottom: lineView.pin(.top, to: .top, of: self)
}
switch location {
case .top, .middle: lineView.pin(.bottom, to: .bottom, of: self)
case .bottom: lineView.bottomAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
let dotView = UIView()
let dotSize = Values.pathRowDotSize
dotView.set(.width, to: dotSize)
dotView.set(.height, to: dotSize)
dotView.layer.cornerRadius = dotSize / 2
dotView.backgroundColor = Colors.text
addSubview(dotView)
dotView.center(in: self)
}
}

View File

@ -2829,3 +2829,5 @@
"Please check your internet connection and try again" = "Please check your internet connection and try again";
"Authorizing Device Link" = "Authorizing Device Link";
"Please wait while the device link is created. This can take up to a minute." = "Please wait while the device link is created. This can take up to a minute.";
"Path" = "Path";
"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." = "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.";

View File

@ -47,6 +47,10 @@ public final class Values : NSObject {
@objc public static let messageBubbleCornerRadius: CGFloat = 10
@objc public static let progressBarThickness: CGFloat = 2
@objc public static let pnOptionCornerRadius = CGFloat(8)
@objc public static let pathStatusViewSize = CGFloat(8)
@objc public static let pathRowLineThickness = CGFloat(1)
@objc public static let pathRowDotSize = CGFloat(8)
@objc public static let pathRowHeight = CGFloat(72)
// MARK: - Distances
@objc public static let verySmallSpacing = CGFloat(4)

View File

@ -1,6 +1,6 @@
/// Either a service node or another client if P2P is enabled.
internal final class LokiAPITarget : NSObject, NSCoding {
public final class LokiAPITarget : NSObject, NSCoding {
internal let address: String
internal let port: UInt16
internal let publicKeySet: KeySet?
@ -27,7 +27,7 @@ internal final class LokiAPITarget : NSObject, NSCoding {
}
// MARK: Coding
internal init?(coder: NSCoder) {
public init?(coder: NSCoder) {
address = coder.decodeObject(forKey: "address") as! String
port = coder.decodeObject(forKey: "port") as! UInt16
if let idKey = coder.decodeObject(forKey: "idKey") as? String, let encryptionKey = coder.decodeObject(forKey: "encryptionKey") as? String {
@ -38,7 +38,7 @@ internal final class LokiAPITarget : NSObject, NSCoding {
super.init()
}
internal func encode(with coder: NSCoder) {
public func encode(with coder: NSCoder) {
coder.encode(address, forKey: "address")
coder.encode(port, forKey: "port")
if let keySet = publicKeySet {
@ -48,16 +48,16 @@ internal final class LokiAPITarget : NSObject, NSCoding {
}
// MARK: Equality
override internal func isEqual(_ other: Any?) -> 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)" }
}

View File

@ -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<LokiAPITarget> = []
/// - Note: Must only be modified from `LokiAPI.workQueue`.
private static var paths: Set<Path> = []
public static var paths: [Path] = []
private static var snodePool: Set<LokiAPITarget> {
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<Set<Path>> {
private static func buildPaths() -> Promise<[Path]> {
print("[Loki] [Onion Request API] Building onion request paths.")
return LokiAPI.getRandomSnode().then(on: LokiAPI.workQueue) { _ -> Promise<Set<Path>> 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
})
}
}
}
}