Merge pull request #197 from loki-project/onion-requests

Onion Requests 2.0
This commit is contained in:
Niels Andriesse 2020-05-28 14:41:15 +10:00 committed by GitHub
commit 5730710cb3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 662 additions and 126 deletions

2
Pods

@ -1 +1 @@
Subproject commit a10cec6cd837552a4b0a88462636cf06494644c4
Subproject commit 731fffac3dcf56c55fc5df68ccc90d1d338f22cc

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 */; };
@ -621,6 +623,8 @@
B90418E6183E9DD40038554A /* DateUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B90418E5183E9DD40038554A /* DateUtil.m */; };
B9EB5ABD1884C002007CBB57 /* MessageUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B9EB5ABC1884C002007CBB57 /* MessageUI.framework */; };
BFF3FB9730634F37D25903F4 /* Pods_Signal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D17BB5C25D615AB49813100C /* Pods_Signal.framework */; };
C31A6C5A247F214E001123EF /* UIView+Glow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31A6C59247F214E001123EF /* UIView+Glow.swift */; };
C31A6C5C247F2CF3001123EF /* CGRect+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */; };
C34C8F7423A7830B00D82669 /* SpaceMono-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */; };
C353F8F7244808E90011121A /* PNModeSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C353F8F6244808E90011121A /* PNModeSheet.swift */; };
C353F8F9244809150011121A /* PNOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C353F8F8244809150011121A /* PNOptionView.swift */; };
@ -1462,6 +1466,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>"; };
@ -1495,6 +1501,8 @@
B97940251832BD2400BD66CB /* UIUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIUtil.h; sourceTree = "<group>"; };
B97940261832BD2400BD66CB /* UIUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIUtil.m; sourceTree = "<group>"; };
B9EB5ABC1884C002007CBB57 /* MessageUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MessageUI.framework; path = System/Library/Frameworks/MessageUI.framework; sourceTree = SDKROOT; };
C31A6C59247F214E001123EF /* UIView+Glow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Glow.swift"; sourceTree = "<group>"; };
C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Utilities.swift"; sourceTree = "<group>"; };
C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "SpaceMono-Bold.ttf"; sourceTree = "<group>"; };
C353F8F6244808E90011121A /* PNModeSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNModeSheet.swift; sourceTree = "<group>"; };
C353F8F8244809150011121A /* PNOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNOptionView.swift; sourceTree = "<group>"; };
@ -2871,6 +2879,7 @@
B8B26C8E234D629C004ED98C /* MentionCandidateSelectionView.swift */,
B8B26C90234D8CBD004ED98C /* MentionCandidateSelectionViewDelegate.swift */,
B83F2B85240C7B8F000A54AB /* NewConversationButtonSet.swift */,
B879D44A247E1D9200DB3608 /* PathStatusView.swift */,
C353F8F8244809150011121A /* PNOptionView.swift */,
B8BB82B02390C37000BA5194 /* SearchBar.swift */,
B85357BE23A1AE0800AAF6CD /* SeedReminderView.swift */,
@ -2891,7 +2900,9 @@
B886B4A82398BA1500211ABE /* QRCode.swift */,
B8783E9D23EB948D00404FB8 /* UILabel+Interaction.swift */,
B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */,
C31A6C59247F214E001123EF /* UIView+Glow.swift */,
C3548F0724456AB6009433A8 /* UIView+Wrapping.swift */,
C31A6C5B247F2CF3001123EF /* CGRect+Utilities.swift */,
);
path = Utilities;
sourceTree = "<group>";
@ -2917,6 +2928,7 @@
B8CCF63623961D6D0091D419 /* NewPrivateChatVC.swift */,
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */,
C3DFFAC723E970080058DAF8 /* OpenGroupSuggestionSheet.swift */,
B879D448247E1BE300DB3608 /* PathVC.swift */,
C353F8F6244808E90011121A /* PNModeSheet.swift */,
C3548F0524456447009433A8 /* PNModeVC.swift */,
B886B4A62398B23E00211ABE /* QRCodeVC.swift */,
@ -4010,6 +4022,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 +4159,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 */,
@ -4195,6 +4209,7 @@
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
457F671B20746193000EABCD /* QuotedReplyPreview.swift in Sources */,
34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */,
C31A6C5C247F2CF3001123EF /* CGRect+Utilities.swift in Sources */,
4C21D5D6223A9DC500EF8A77 /* UIAlerts+iOS9.m in Sources */,
34DBF004206BD5A500025978 /* OWSBubbleView.m in Sources */,
3496957021A301A100DCFE74 /* OWSBackupIO.m in Sources */,
@ -4209,6 +4224,7 @@
34BECE2B1F74C12700D7438D /* DebugUIStress.m in Sources */,
340FC8B9204DAC8D007AEB0F /* UpdateGroupViewController.m in Sources */,
B8BB82A5238F627000BA5194 /* HomeVC.swift in Sources */,
C31A6C5A247F214E001123EF /* UIView+Glow.swift in Sources */,
3448E1662215B313004B052E /* OnboardingCaptchaViewController.swift in Sources */,
4574A5D61DD6704700C6B692 /* CallService.swift in Sources */,
4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */,

View file

@ -1519,7 +1519,7 @@ static NSTimeInterval launchStartedAt;
[ThreadUtil deleteAllContent];
[SSKEnvironment.shared.messageSenderJobQueue clearAllJobs];
[SSKEnvironment.shared.identityManager clearIdentityKey];
[LKAPI clearRandomSnodePool];
[LKAPI clearSnodePool];
[self stopPollerIfNeeded];
[self stopOpenGroupPollersIfNeeded];
[self.lokiNewsFeedPoller stop];

View file

@ -158,7 +158,9 @@ final class NewConversationButtonSet : UIView {
self.layoutIfNeeded()
button.frame = frame
button.layer.cornerRadius = size / 2
button.setGlow(to: size, with: Colors.newConversationButtonShadow, animated: true)
let glowColor = Colors.newConversationButtonShadow
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: true, radius: isLightMode ? 4 : 6)
button.setCircularGlow(with: glowConfiguration)
button.backgroundColor = Colors.accent
}
}
@ -183,7 +185,8 @@ final class NewConversationButtonSet : UIView {
button.frame = frame
button.layer.cornerRadius = size / 2
let glowColor = isLightMode ? UIColor.black.withAlphaComponent(0.4) : UIColor.black
button.setGlow(to: size, with: glowColor, animated: true)
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: true, radius: isLightMode ? 4 : 6)
button.setCircularGlow(with: glowConfiguration)
button.backgroundColor = Colors.newConversationButtonCollapsedBackground
}
}
@ -208,8 +211,7 @@ private final class NewConversationButton : UIImageView {
private let icon: UIImage
var widthConstraint: NSLayoutConstraint!
var heightConstraint: NSLayoutConstraint!
// Initialization
init(isMainButton: Bool, icon: UIImage) {
self.isMainButton = isMainButton
self.icon = icon
@ -230,7 +232,8 @@ private final class NewConversationButton : UIImageView {
let size = Values.newConversationButtonCollapsedSize
layer.cornerRadius = size / 2
let glowColor = isMainButton ? Colors.newConversationButtonShadow : (isLightMode ? UIColor.black.withAlphaComponent(0.4) : UIColor.black)
setGlow(to: size, with: glowColor, animated: false)
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: false, radius: isLightMode ? 4 : 6)
setCircularGlow(with: glowConfiguration)
layer.masksToBounds = false
let iconColor = (isMainButton && isLightMode) ? UIColor.white : Colors.text
image = icon.asTintedImage(color: iconColor)!
@ -238,31 +241,6 @@ private final class NewConversationButton : UIImageView {
widthConstraint = set(.width, to: size)
heightConstraint = set(.height, to: size)
}
// General
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 ? 4 : 6
}
}
// MARK: Convenience
@ -306,13 +284,3 @@ private extension CGPoint {
return sqrt(pow(self.x - otherPoint.x, 2) + pow(self.y - otherPoint.y, 2))
}
}
private extension CGRect {
init(center: CGPoint, size: CGSize) {
let originX = center.x - size.width / 2
let originY = center.y - size.height / 2
let origin = CGPoint(x: originX, y: originY)
self.init(origin: origin, size: size)
}
}

View file

@ -0,0 +1,53 @@
final class PathStatusView : UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setUpViewHierarchy()
registerObservers()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setUpViewHierarchy()
registerObservers()
}
private func setUpViewHierarchy() {
layer.cornerRadius = Values.pathStatusViewSize / 2
layer.masksToBounds = false
if OnionRequestAPI.paths.count < OnionRequestAPI.pathCount {
let storage = OWSPrimaryStorage.shared()
storage.dbReadConnection.read { transaction in
OnionRequestAPI.paths = storage.getOnionRequestPaths(in: transaction)
}
}
let color = (OnionRequestAPI.paths.count >= OnionRequestAPI.pathCount) ? Colors.accent : Colors.pathsBuilding
setColor(to: color, isAnimated: false)
}
private func registerObservers() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(handleBuildingPathsNotification), name: .buildingPaths, object: nil)
notificationCenter.addObserver(self, selector: #selector(handlePathsBuiltNotification), name: .pathsBuilt, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
private func setColor(to color: UIColor, isAnimated: Bool) {
backgroundColor = color
let size = Values.pathStatusViewSize
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: color, isAnimated: isAnimated, radius: isLightMode ? 6 : 8)
setCircularGlow(with: glowConfiguration)
}
@objc private func handleBuildingPathsNotification() {
setColor(to: Colors.pathsBuilding, isAnimated: true)
}
@objc private func handlePathsBuiltNotification() {
setColor(to: Colors.accent, isAnimated: true)
}
}

View file

@ -0,0 +1,10 @@
extension CGRect {
init(center: CGPoint, size: CGSize) {
let originX = center.x - size.width / 2
let originY = center.y - size.height / 2
let origin = CGPoint(x: originX, y: originY)
self.init(origin: origin, size: size)
}
}

View file

@ -0,0 +1,46 @@
extension UIView {
struct CircularGlowConfiguration {
let size: CGFloat
let color: UIColor
let isAnimated: Bool
let offset: CGSize
let opacity: Float
let radius: CGFloat
init(size: CGFloat, color: UIColor, isAnimated: Bool, offset: CGSize = CGSize(width: 0, height: 0.8), opacity: Float = isLightMode ? 0.4 : 1, radius: CGFloat) {
self.size = size
self.color = color
self.isAnimated = isAnimated
self.offset = offset
self.opacity = opacity
self.radius = radius
}
}
func setCircularGlow(with configuration: CircularGlowConfiguration) {
let newSize = configuration.size
let newPath = UIBezierPath(ovalIn: CGRect(origin: CGPoint.zero, size: CGSize(width: newSize, height: newSize))).cgPath
if configuration.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 = configuration.color.cgColor
if configuration.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 = configuration.offset
layer.shadowOpacity = configuration.opacity
layer.shadowRadius = configuration.radius
}
}

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,304 @@
import NVActivityIndicatorView
final class PathVC : BaseVC {
// MARK: Components
private lazy var pathStackView: UIStackView = {
let result = UIStackView()
result.axis = .vertical
return result
}()
private lazy var spinner: NVActivityIndicatorView = {
let result = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil)
result.set(.width, to: 64)
result.set(.height, to: 64)
return result
}()
private lazy var rebuildPathButton: Button = {
let result = Button(style: .prominentOutline, size: .large)
result.setTitle(NSLocalizedString("Rebuild Path", comment: ""), for: UIControl.State.normal)
result.addTarget(self, action: #selector(rebuildPath), for: UIControl.Event.touchUpInside)
return result
}()
// MARK: Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
setUpBackground()
setUpNavBar()
setUpViewHierarchy()
registerObservers()
}
private func setUpBackground() {
view.backgroundColor = .clear
let gradient = Gradients.defaultLokiBackground
view.setGradient(gradient)
}
private func setUpNavBar() {
// Set up navigation bar style
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
}
private func setUpViewHierarchy() {
// 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 bouncing your messages through several Service Nodes in Sessions decentralized network. These are the Service Nodes currently being used by your device:", comment: "")
explanationLabel.numberOfLines = 0
explanationLabel.textAlignment = .center
explanationLabel.lineBreakMode = .byWordWrapping
// Set up path stack view
let pathStackViewContainer = UIView()
pathStackViewContainer.addSubview(pathStackView)
pathStackView.pin([ UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: pathStackViewContainer)
pathStackView.center(in: pathStackViewContainer)
pathStackView.leadingAnchor.constraint(greaterThanOrEqualTo: pathStackViewContainer.leadingAnchor).isActive = true
pathStackViewContainer.trailingAnchor.constraint(greaterThanOrEqualTo: pathStackView.trailingAnchor).isActive = true
pathStackViewContainer.addSubview(spinner)
spinner.leadingAnchor.constraint(greaterThanOrEqualTo: pathStackViewContainer.leadingAnchor).isActive = true
spinner.topAnchor.constraint(greaterThanOrEqualTo: pathStackViewContainer.topAnchor).isActive = true
pathStackViewContainer.trailingAnchor.constraint(greaterThanOrEqualTo: spinner.trailingAnchor).isActive = true
pathStackViewContainer.bottomAnchor.constraint(greaterThanOrEqualTo: spinner.bottomAnchor).isActive = true
spinner.center(in: pathStackViewContainer)
// Set up rebuild path button
let rebuildPathButtonContainer = UIView()
rebuildPathButtonContainer.addSubview(rebuildPathButton)
rebuildPathButton.pin(.leading, to: .leading, of: rebuildPathButtonContainer, withInset: 80)
rebuildPathButton.pin(.top, to: .top, of: rebuildPathButtonContainer)
rebuildPathButtonContainer.pin(.trailing, to: .trailing, of: rebuildPathButton, withInset: 80)
rebuildPathButtonContainer.pin(.bottom, to: .bottom, of: rebuildPathButton)
// Set up spacers
let topSpacer = UIView.vStretchingSpacer()
let bottomSpacer = UIView.vStretchingSpacer()
// Set up main stack view
let mainStackView = UIStackView(arrangedSubviews: [ explanationLabel, topSpacer, pathStackViewContainer, bottomSpacer, rebuildPathButtonContainer ])
mainStackView.axis = .vertical
mainStackView.alignment = .fill
mainStackView.layoutMargins = UIEdgeInsets(top: Values.largeSpacing, left: Values.largeSpacing, bottom: Values.largeSpacing, right: Values.largeSpacing)
mainStackView.isLayoutMarginsRelativeArrangement = true
view.addSubview(mainStackView)
mainStackView.pin(to: view)
// Set up spacer constraints
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor).isActive = true
// Perform initial update
update()
}
private func registerObservers() {
let notificationCenter = NotificationCenter.default
notificationCenter.addObserver(self, selector: #selector(handleBuildingPathsNotification), name: .buildingPaths, object: nil)
notificationCenter.addObserver(self, selector: #selector(handlePathsBuiltNotification), name: .pathsBuilt, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: Updating
@objc private func handleBuildingPathsNotification() { update() }
@objc private func handlePathsBuiltNotification() { update() }
private func update() {
pathStackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
if OnionRequestAPI.paths.count >= OnionRequestAPI.pathCount {
let pathToDisplay = OnionRequestAPI.paths.first!
let dotAnimationRepeatInterval = Double(pathToDisplay.count) + 2
let snodeRows: [UIStackView] = pathToDisplay.enumerated().map { index, snode in
let isGuardSnode = (snode == pathToDisplay.first!)
return getPathRow(snode: snode, location: .middle, dotAnimationStartDelay: Double(index) + 2, dotAnimationRepeatInterval: dotAnimationRepeatInterval, isGuardSnode: isGuardSnode)
}
let youRow = getPathRow(title: NSLocalizedString("You", comment: ""), subtitle: nil, location: .top, dotAnimationStartDelay: 1, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
let destinationRow = getPathRow(title: NSLocalizedString("Destination", comment: ""), subtitle: nil, location: .bottom, dotAnimationStartDelay: Double(pathToDisplay.count) + 2, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
let rows = [ youRow ] + snodeRows + [ destinationRow ]
rows.forEach { pathStackView.addArrangedSubview($0) }
spinner.stopAnimating()
UIView.animate(withDuration: 0.25) {
self.spinner.alpha = 0
self.rebuildPathButton.layer.borderColor = Colors.accent.cgColor
self.rebuildPathButton.setTitleColor(Colors.accent, for: UIControl.State.normal)
}
rebuildPathButton.isEnabled = true
} else {
spinner.startAnimating()
UIView.animate(withDuration: 0.25) {
self.spinner.alpha = 1
self.rebuildPathButton.layer.borderColor = Colors.text.withAlphaComponent(Values.unimportantElementOpacity).cgColor
self.rebuildPathButton.setTitleColor(Colors.text.withAlphaComponent(Values.unimportantElementOpacity), for: UIControl.State.normal)
}
rebuildPathButton.isEnabled = false
}
}
// MARK: General
private func getPathRow(title: String, subtitle: String?, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double) -> UIStackView {
let lineView = LineView(location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
lineView.set(.width, to: Values.pathRowDotSize)
lineView.set(.height, to: Values.pathRowHeight)
let titleLabel = UILabel()
titleLabel.textColor = Colors.text
titleLabel.font = .systemFont(ofSize: Values.mediumFontSize)
titleLabel.text = title
titleLabel.lineBreakMode = .byTruncatingTail
let titleStackView = UIStackView(arrangedSubviews: [ titleLabel ])
titleStackView.axis = .vertical
if let subtitle = subtitle {
let subtitleLabel = UILabel()
subtitleLabel.textColor = Colors.text
subtitleLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
subtitleLabel.text = subtitle
subtitleLabel.lineBreakMode = .byTruncatingTail
titleStackView.addArrangedSubview(subtitleLabel)
}
let stackView = UIStackView(arrangedSubviews: [ lineView, titleStackView ])
stackView.axis = .horizontal
stackView.spacing = Values.largeSpacing
stackView.alignment = .center
return stackView
}
private func getPathRow(snode: LokiAPITarget, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView {
var snodeIP = snode.description
if snodeIP.hasPrefix("https://") { snodeIP.removeFirst(8) }
if let colonIndex = snodeIP.lastIndex(of: ":") {
snodeIP = String(snodeIP[snodeIP.startIndex..<colonIndex])
}
let title = isGuardSnode ? NSLocalizedString("Guard Node", comment: "") : NSLocalizedString("Service Node", comment: "")
return getPathRow(title: title, subtitle: snodeIP, location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval)
}
// MARK: Interaction
@objc private func close() {
dismiss(animated: true, completion: nil)
}
@objc private func rebuildPath() {
OnionRequestAPI.guardSnodes = []
OnionRequestAPI.paths = []
let _ = OnionRequestAPI.buildPaths()
}
}
// MARK: Line View
private final class LineView : UIView {
private let location: Location
private let dotAnimationStartDelay: Double
private let dotAnimationRepeatInterval: Double
private var dotViewWidthConstraint: NSLayoutConstraint!
private var dotViewHeightConstraint: NSLayoutConstraint!
private var dotViewAnimationTimer: Timer!
enum Location {
case top, middle, bottom
}
private lazy var dotView: UIView = {
let result = UIView()
result.layer.cornerRadius = Values.pathRowDotSize / 2
let glowConfiguration = UIView.CircularGlowConfiguration(size: Values.pathRowDotSize, color: Colors.accent, isAnimated: true, radius: isLightMode ? 2 : 4)
result.setCircularGlow(with: glowConfiguration)
result.backgroundColor = Colors.accent
return result
}()
init(location: Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double) {
self.location = location
self.dotAnimationStartDelay = dotAnimationStartDelay
self.dotAnimationRepeatInterval = dotAnimationRepeatInterval
super.init(frame: CGRect.zero)
setUpViewHierarchy()
}
override init(frame: CGRect) {
preconditionFailure("Use init(location:dotAnimationStartDelay:dotAnimationRepeatInterval:) instead.")
}
required init?(coder: NSCoder) {
preconditionFailure("Use init(location:dotAnimationStartDelay:dotAnimationRepeatInterval:) instead.")
}
private func setUpViewHierarchy() {
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 dotSize = Values.pathRowDotSize
dotViewWidthConstraint = dotView.set(.width, to: dotSize)
dotViewHeightConstraint = dotView.set(.height, to: dotSize)
addSubview(dotView)
dotView.center(in: self)
Timer.scheduledTimer(withTimeInterval: dotAnimationStartDelay, repeats: false) { [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.animate()
strongSelf.dotViewAnimationTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.dotAnimationRepeatInterval, repeats: true) { _ in
guard let strongSelf = self else { return }
strongSelf.animate()
}
}
}
deinit {
dotViewAnimationTimer?.invalidate()
}
private func animate() {
expandDot()
Timer.scheduledTimer(withTimeInterval: 1, repeats: false) { [weak self] _ in
self?.collapseDot()
}
}
private func expandDot() {
let newSize = Values.pathRowExpandedDotSize
let newGlowRadius: CGFloat = isLightMode ? 6 : 8
updateDotView(size: newSize, glowRadius: newGlowRadius)
}
private func collapseDot() {
let newSize = Values.pathRowDotSize
let newGlowRadius: CGFloat = isLightMode ? 2 : 4
updateDotView(size: newSize, glowRadius: newGlowRadius)
}
private func updateDotView(size: CGFloat, glowRadius: CGFloat) {
let frame = CGRect(center: dotView.center, size: CGSize(width: size, height: size))
dotViewWidthConstraint.constant = size
dotViewHeightConstraint.constant = size
UIView.animate(withDuration: 0.5) {
self.layoutIfNeeded()
self.dotView.frame = frame
self.dotView.layer.cornerRadius = size / 2
let glowColor = Colors.accent
let glowConfiguration = UIView.CircularGlowConfiguration(size: size, color: glowColor, isAnimated: true, radius: glowRadius)
self.dotView.setCircularGlow(with: glowConfiguration)
self.dotView.backgroundColor = Colors.accent
}
}
}

View file

@ -207,11 +207,11 @@ final class RegisterVC : BaseVC {
let touchInLegalLabelCoordinates = tapGestureRecognizer.location(in: legalLabel)
let characterIndex = legalLabel.characterIndex(for: touchInLegalLabelCoordinates)
if tosRange.contains(characterIndex) {
urlAsString = "https://getsession.org/legal/#tos"
urlAsString = "https://getsession.org/terms-of-service/"
} else if eulaRange.contains(characterIndex) {
urlAsString = "https://getsession.org/legal/#eula"
urlAsString = "https://getsession.org/terms-of-service/#eula"
} else if ppRange.contains(characterIndex) {
urlAsString = "https://getsession.org/legal/#privacy-policy"
urlAsString = "https://getsession.org/privacy-policy/"
} else {
urlAsString = nil
}

View file

@ -196,11 +196,11 @@ final class RestoreVC : BaseVC {
let touchInLegalLabelCoordinates = tapGestureRecognizer.location(in: legalLabel)
let characterIndex = legalLabel.characterIndex(for: touchInLegalLabelCoordinates)
if tosRange.contains(characterIndex) {
urlAsString = "https://getsession.org/legal/#tos"
urlAsString = "https://getsession.org/terms-of-service/"
} else if eulaRange.contains(characterIndex) {
urlAsString = "https://getsession.org/legal/#eula"
urlAsString = "https://getsession.org/terms-of-service/#eula"
} else if ppRange.contains(characterIndex) {
urlAsString = "https://getsession.org/legal/#privacy-policy"
urlAsString = "https://getsession.org/privacy-policy/"
} else {
urlAsString = nil
}

View file

@ -679,7 +679,7 @@ typedef NS_ENUM(NSInteger, HomeViewControllerSection) {
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
[ThreadUtil deleteAllContent];
[SSKEnvironment.shared.identityManager clearIdentityKey];
[LKAPI clearRandomSnodePool];
[LKAPI clearSnodePool];
AppDelegate *appDelegate = (AppDelegate *)UIApplication.sharedApplication.delegate;
[appDelegate stopPollerIfNeeded];
[appDelegate stopOpenGroupPollersIfNeeded];

View file

@ -2552,7 +2552,9 @@
/* Info message embedding a {{time amount}}, see the *_TIME_AMOUNT strings for context. */
"YOU_UPDATED_DISAPPEARING_MESSAGES_CONFIGURATION" = "You set disappearing message time to %@";
// Loki:
// MARK: - Loki Messenger:
"Session can let you know when you get a message (and who it is from)" = "Session can let you know when you get a message (and who it is from)";
"Create Your Session Account" = "Create Your Session Account";
"Enter a name to be shown to your contacts" = "Enter a name to be shown to your contacts";
@ -2683,7 +2685,9 @@
"Restore session" = "Restore session";
"Would you like to start a new session with %@?" = "Would you like to start a new session with %@?";
// MARK: - Redesign
// MARK: - Session
"Messages" = "Messages";
"Note to Self" = "Note to Self";
"New Group" = "New Group";
@ -2829,3 +2833,10 @@
"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 bouncing your messages through several Service Nodes in Sessions decentralized network. These are the Service Nodes currently being used by your device:" = "Session hides your IP by bouncing your messages through several Service Nodes in Sessions decentralized network. These are the Service Nodes currently being used by your device:";
"Guard Node" = "Guard Node";
"Service Node" = "Service Node";
"You" = "You";
"Destination" = "Destination";
"Rebuild Path" = "Rebuild Path";

View file

@ -38,4 +38,5 @@ public final class Colors : NSObject {
@objc public static var newConversationButtonCollapsedBackground = isLightMode ? UIColor(hex: 0xF5F5F5) : UIColor(hex: 0x1F1F1F)
@objc public static var pnOptionBackground = isLightMode ? UIColor(hex: 0xFCFCFC) : UIColor(hex: 0x1B1B1B)
@objc public static var pnOptionBorder = UIColor(hex: 0x212121)
@objc public static var pathsBuilding = UIColor(hex: 0xFFCE3A)
}

View file

@ -47,6 +47,11 @@ 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 var pathRowLineThickness: CGFloat { return 1 / UIScreen.main.scale }
@objc public static let pathRowDotSize = CGFloat(8)
@objc public static let pathRowExpandedDotSize = CGFloat(16)
@objc public static let pathRowHeight = isSmallScreen ? CGFloat(52) : CGFloat(75)
// MARK: - Distances
@objc public static let verySmallSpacing = CGFloat(4)

View file

@ -26,15 +26,26 @@ public extension LokiAPI {
// MARK: Clearnet Setup
fileprivate static let seedNodePool: Set<String> = [ "https://storage.seed1.loki.network", "https://storage.seed3.loki.network", "https://public.loki.foundation" ]
internal static var randomSnodePool: Set<LokiAPITarget> = []
internal static var snodePool: Set<LokiAPITarget> = []
@objc public static func clearRandomSnodePool() {
randomSnodePool.removeAll()
@objc public static func clearSnodePool() {
snodePool.removeAll()
// Dispatch async on the main queue to avoid nested write transactions
DispatchQueue.main.async {
storage.dbReadWriteConnection.readWrite { transaction in
storage.clearSnodePool(in: transaction)
}
}
}
// MARK: Internal API
internal static func getRandomSnode() -> Promise<LokiAPITarget> {
if randomSnodePool.isEmpty {
if snodePool.isEmpty {
storage.dbReadConnection.read { transaction in
snodePool = storage.getSnodePool(in: transaction)
}
}
if snodePool.isEmpty {
let target = seedNodePool.randomElement()!
let url = "\(target)/json_rpc"
let parameters: JSON = [
@ -42,19 +53,16 @@ public extension LokiAPI {
"params" : [
"active_only" : true,
"fields" : [
"public_ip" : true,
"storage_port" : true,
"pubkey_ed25519" : true,
"pubkey_x25519" : true
"public_ip" : true, "storage_port" : true, "pubkey_ed25519" : true, "pubkey_x25519" : true
]
]
]
print("[Loki] Populating snode pool using: \(target).")
let (promise, seal) = Promise<LokiAPITarget>.pending()
attempt(maxRetryCount: 4, recoveringOn: DispatchQueue.global()) {
HTTP.execute(.post, url, parameters: parameters).map(on: DispatchQueue.global()) { json in
HTTP.execute(.post, url, parameters: parameters).map(on: DispatchQueue.global()) { json -> LokiAPITarget in
guard let intermediate = json["result"] as? JSON, let rawTargets = intermediate["service_node_states"] as? [JSON] else { throw LokiAPIError.randomSnodePoolUpdatingFailed }
randomSnodePool = try Set(rawTargets.flatMap { rawTarget in
snodePool = try Set(rawTargets.flatMap { rawTarget in
guard let address = rawTarget["public_ip"] as? String, let port = rawTarget["storage_port"] as? Int, let ed25519PublicKey = rawTarget["pubkey_ed25519"] as? String, let x25519PublicKey = rawTarget["pubkey_x25519"] as? String, address != "0.0.0.0" else {
print("[Loki] Failed to parse target from: \(rawTarget).")
return nil
@ -62,10 +70,17 @@ public extension LokiAPI {
return LokiAPITarget(address: "https://\(address)", port: UInt16(port), publicKeySet: LokiAPITarget.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey))
})
// randomElement() uses the system's default random generator, which is cryptographically secure
return randomSnodePool.randomElement()!
return snodePool.randomElement()!
}
}.done(on: DispatchQueue.global()) { snode in
seal.fulfill(snode)
// Dispatch async on the main queue to avoid nested write transactions
DispatchQueue.main.async {
storage.dbReadWriteConnection.readWrite { transaction in
print("[Loki] Persisting snode pool to database.")
storage.setSnodePool(LokiAPI.snodePool, in: transaction)
}
}
}.catch(on: DispatchQueue.global()) { error in
print("[Loki] Failed to contact seed node at: \(target).")
seal.reject(error)
@ -74,7 +89,7 @@ public extension LokiAPI {
} else {
return Promise<LokiAPITarget> { seal in
// randomElement() uses the system's default random generator, which is cryptographically secure
seal.fulfill(randomSnodePool.randomElement()!)
seal.fulfill(snodePool.randomElement()!)
}
}
}
@ -161,7 +176,14 @@ internal extension Promise {
if newFailureCount >= LokiAPI.failureThreshold {
print("[Loki] Failure threshold reached for: \(target); dropping it.")
LokiAPI.dropIfNeeded(target, hexEncodedPublicKey: hexEncodedPublicKey) // Remove it from the swarm cache associated with the given public key
LokiAPI.randomSnodePool.remove(target) // Remove it from the random snode pool
LokiAPI.snodePool.remove(target) // Remove it from the snode pool
// Dispatch async on the main queue to avoid nested write transactions
DispatchQueue.main.async {
let storage = OWSPrimaryStorage.shared()
storage.dbReadWriteConnection.readWrite { transaction in
storage.dropSnode(target, in: transaction)
}
}
LokiAPI.failureCount[target] = 0
}
case 406:

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,21 +2,21 @@ 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> = []
public 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)
return LokiAPI.randomSnodePool.subtracting(unreliableSnodes)
return LokiAPI.snodePool.subtracting(unreliableSnodes)
}
// MARK: Settings
private static let pathCount: UInt = 2
/// The number of snodes (including the guard snode) in a path.
private static let pathSize: UInt = 1
private static let pathSize: UInt = 3
public static let pathCount: UInt = 2
private static var guardSnodeCount: UInt { return pathCount } // One per path
@ -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,18 @@ 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>> {
public 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 getGuardSnodes().map(on: LokiAPI.workQueue) { guardSnodes in
DispatchQueue.main.async {
NotificationCenter.default.post(name: .buildingPaths, object: nil)
}
return LokiAPI.getRandomSnode().then(on: LokiAPI.workQueue) { _ -> Promise<[Path]> in // Just used to populate the snode pool
return getGuardSnodes().map(on: LokiAPI.workQueue) { guardSnodes -> [Path] 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 +120,19 @@ internal enum OnionRequestAPI {
}
print("[Loki] [Onion Request API] Built new onion request path: \(result.prettifiedDescription).")
return result
})
}
}.map(on: LokiAPI.workQueue) { paths in
OnionRequestAPI.paths = paths
// Dispatch async on the main queue to avoid nested write transactions
DispatchQueue.main.async {
let storage = OWSPrimaryStorage.shared()
storage.dbReadWriteConnection.readWrite { transaction in
print("[Loki] Persisting onion request paths to database.")
storage.setOnionRequestPaths(paths, in: transaction)
}
NotificationCenter.default.post(name: .pathsBuilt, object: nil)
}
return paths
}
}
}
@ -127,6 +142,12 @@ internal enum OnionRequestAPI {
/// - Note: Exposed for testing purposes.
internal static func getPath(excluding snode: LokiAPITarget) -> Promise<Path> {
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
if paths.count < pathCount {
let storage = OWSPrimaryStorage.shared()
storage.dbReadConnection.read { transaction in
paths = storage.getOnionRequestPaths(in: transaction)
}
}
// randomElement() uses the system's default random generator, which is cryptographically secure
if paths.count >= pathCount {
return Promise<Path> { seal in
@ -134,15 +155,20 @@ internal enum OnionRequestAPI {
}
} else {
return buildPaths().map(on: LokiAPI.workQueue) { paths in
let path = paths.filter { !$0.contains(snode) }.randomElement()!
OnionRequestAPI.paths = paths
return path
return paths.filter { !$0.contains(snode) }.randomElement()!
}
}
}
private static func dropPath(containing snode: LokiAPITarget) {
paths = paths.filter { !$0.contains(snode) }
private static func dropPaths() {
paths.removeAll()
// Dispatch async on the main queue to avoid nested write transactions
DispatchQueue.main.async {
let storage = OWSPrimaryStorage.shared()
storage.dbReadWriteConnection.readWrite { transaction in
storage.clearOnionRequestPaths(in: transaction)
}
}
}
private static func dropGuardSnode(_ snode: LokiAPITarget) {
@ -224,7 +250,7 @@ internal enum OnionRequestAPI {
}
promise.catch(on: LokiAPI.workQueue) { error in // Must be invoked on LokiAPI.workQueue
guard case HTTP.Error.httpRequestFailed(_, _) = error else { return }
dropPath(containing: guardSnode) // A snode in the path is bad; retry with a different path
dropPaths() // A snode in the path is bad; retry with a different path
dropGuardSnode(guardSnode)
}
promise.handlingErrorsIfNeeded(forTargetSnode: snode, associatedWith: hexEncodedPublicKey)
@ -250,7 +276,14 @@ private extension Promise where T == JSON {
if newFailureCount >= LokiAPI.failureThreshold {
print("[Loki] Failure threshold reached for: \(snode); dropping it.")
LokiAPI.dropIfNeeded(snode, hexEncodedPublicKey: hexEncodedPublicKey) // Remove it from the swarm cache associated with the given public key
LokiAPI.randomSnodePool.remove(snode) // Remove it from the random snode pool
LokiAPI.snodePool.remove(snode) // Remove it from the snode pool
// Dispatch async on the main queue to avoid nested write transactions
DispatchQueue.main.async {
let storage = OWSPrimaryStorage.shared()
storage.dbReadWriteConnection.readWrite { transaction in
storage.dropSnode(snode, in: transaction)
}
}
LokiAPI.failureCount[snode] = 0
}
case 406:

View file

@ -1,7 +1,72 @@
public extension OWSPrimaryStorage {
// MARK: Session Requests
// MARK: - Snode Pool
private static let snodePoolCollection = "LokiSnodePoolCollection"
public func setSnodePool(_ snodePool: Set<LokiAPITarget>, in transaction: YapDatabaseReadWriteTransaction) {
clearSnodePool(in: transaction)
snodePool.forEach { snode in
transaction.setObject(snode, forKey: snode.description, inCollection: OWSPrimaryStorage.snodePoolCollection)
}
}
public func clearSnodePool(in transaction: YapDatabaseReadWriteTransaction) {
transaction.removeAllObjects(inCollection: OWSPrimaryStorage.snodePoolCollection)
}
public func getSnodePool(in transaction: YapDatabaseReadTransaction) -> Set<LokiAPITarget> {
var result: Set<LokiAPITarget> = []
transaction.enumerateKeysAndObjects(inCollection: OWSPrimaryStorage.snodePoolCollection) { _, object, _ in
guard let snode = object as? LokiAPITarget else { return }
result.insert(snode)
}
return result
}
public func dropSnode(_ snode: LokiAPITarget, in transaction: YapDatabaseReadWriteTransaction) {
transaction.removeObject(forKey: snode.description, inCollection: OWSPrimaryStorage.snodePoolCollection)
}
// MARK: - Onion Request Path
private static let onionRequestPathCollection = "LokiOnionRequestPathCollection"
public func setOnionRequestPaths(_ paths: [OnionRequestAPI.Path], in transaction: YapDatabaseReadWriteTransaction) {
// FIXME: This is a bit of a dirty approach that assumes 2 paths of length 3 each. We should do better than this.
guard paths.count == 2 else { return }
let path0 = paths[0]
let path1 = paths[1]
guard path0.count == 3, path1.count == 3 else { return }
let collection = OWSPrimaryStorage.onionRequestPathCollection
transaction.setObject(path0[0], forKey: "0-0", inCollection: collection)
transaction.setObject(path0[1], forKey: "0-1", inCollection: collection)
transaction.setObject(path0[2], forKey: "0-2", inCollection: collection)
transaction.setObject(path1[0], forKey: "1-0", inCollection: collection)
transaction.setObject(path1[1], forKey: "1-1", inCollection: collection)
transaction.setObject(path1[2], forKey: "1-2", inCollection: collection)
}
public func getOnionRequestPaths(in transaction: YapDatabaseReadTransaction) -> [OnionRequestAPI.Path] {
let collection = OWSPrimaryStorage.onionRequestPathCollection
guard
let path0Snode0 = transaction.object(forKey: "0-0", inCollection: collection) as? LokiAPITarget,
let path0Snode1 = transaction.object(forKey: "0-1", inCollection: collection) as? LokiAPITarget,
let path0Snode2 = transaction.object(forKey: "0-2", inCollection: collection) as? LokiAPITarget,
let path1Snode0 = transaction.object(forKey: "1-0", inCollection: collection) as? LokiAPITarget,
let path1Snode1 = transaction.object(forKey: "1-1", inCollection: collection) as? LokiAPITarget,
let path1Snode2 = transaction.object(forKey: "1-2", inCollection: collection) as? LokiAPITarget else { return [] }
return [ [ path0Snode0, path0Snode1, path0Snode2 ], [ path1Snode0, path1Snode1, path1Snode2 ] ]
}
public func clearOnionRequestPaths(in transaction: YapDatabaseReadWriteTransaction) {
transaction.removeAllObjects(inCollection: OWSPrimaryStorage.onionRequestPathCollection)
}
// MARK: - Session Requests
private static let sessionRequestTimestampCollection = "LokiSessionRequestTimestampCollection"
public func setSessionRequestTimestamp(for publicKey: String, to timestamp: Date, in transaction: YapDatabaseReadWriteTransaction) {
@ -12,7 +77,9 @@ public extension OWSPrimaryStorage {
transaction.date(forKey: publicKey, inCollection: OWSPrimaryStorage.sessionRequestTimestampCollection)
}
// MARK: Multi Device
// MARK: - Multi Device
private static var deviceLinkCache: Set<DeviceLink> = []
private func getDeviceLinkCollection(for masterHexEncodedPublicKey: String) -> String {
@ -24,63 +91,39 @@ public extension OWSPrimaryStorage {
}
public func setDeviceLinks(_ deviceLinks: Set<DeviceLink>, in transaction: YapDatabaseReadWriteTransaction) {
// TODO: Clear collections first?
deviceLinks.forEach { addDeviceLink($0, in: transaction) } // TODO: Check the performance impact of this
deviceLinks.forEach { addDeviceLink($0, in: transaction) }
}
public func addDeviceLink(_ deviceLink: DeviceLink, in transaction: YapDatabaseReadWriteTransaction) {
OWSPrimaryStorage.deviceLinkCache.insert(deviceLink)
/*
let collection = getDeviceLinkCollection(for: deviceLink.master.hexEncodedPublicKey)
transaction.setObject(deviceLink, forKey: deviceLink.slave.hexEncodedPublicKey, inCollection: collection)
*/
}
public func removeDeviceLink(_ deviceLink: DeviceLink, in transaction: YapDatabaseReadWriteTransaction) {
OWSPrimaryStorage.deviceLinkCache.remove(deviceLink)
/*
let collection = getDeviceLinkCollection(for: deviceLink.master.hexEncodedPublicKey)
transaction.removeObject(forKey: deviceLink.slave.hexEncodedPublicKey, inCollection: collection)
*/
}
public func getDeviceLinks(for masterHexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> Set<DeviceLink> {
return OWSPrimaryStorage.deviceLinkCache.filter { $0.master.hexEncodedPublicKey == masterHexEncodedPublicKey }
/*
let collection = getDeviceLinkCollection(for: masterHexEncodedPublicKey)
guard !transaction.allKeys(inCollection: collection).isEmpty else { return [] } // Fixes a crash that used to occur on Josh's device
var result: Set<DeviceLink> = []
transaction.enumerateRows(inCollection: collection) { _, object, _, _ in
guard let deviceLink = object as? DeviceLink else { return }
result.insert(deviceLink)
}
return result
*/
}
public func getDeviceLink(for slaveHexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> DeviceLink? {
return OWSPrimaryStorage.deviceLinkCache.filter { $0.slave.hexEncodedPublicKey == slaveHexEncodedPublicKey }.first
/*
let query = YapDatabaseQuery(string: "WHERE \(DeviceLinkIndex.slaveHexEncodedPublicKey) = ?", parameters: [ slaveHexEncodedPublicKey ])
let deviceLinks = DeviceLinkIndex.getDeviceLinks(for: query, in: transaction)
guard deviceLinks.count <= 1 else {
print("[Loki] Found multiple device links for slave hex encoded public key: \(slaveHexEncodedPublicKey).")
return nil
}
return deviceLinks.first
*/
}
public func getMasterHexEncodedPublicKey(for slaveHexEncodedPublicKey: String, in transaction: YapDatabaseReadTransaction) -> String? {
return getDeviceLink(for: slaveHexEncodedPublicKey, in: transaction)?.master.hexEncodedPublicKey
}
// MARK: Open Groups
// MARK: - Open Groups
private static let openGroupUserCountCollection = "LokiPublicChatUserCountCollection"
public func getUserCount(for publicChat: LokiPublicChat, in transaction: YapDatabaseReadTransaction) -> Int? {
return transaction.object(forKey: publicChat.id, inCollection: "LokiPublicChatUserCountCollection") as? Int
return transaction.object(forKey: publicChat.id, inCollection: OWSPrimaryStorage.openGroupUserCountCollection) as? Int
}
public func setUserCount(_ userCount: Int, forPublicChatWithID publicChatID: String, in transaction: YapDatabaseReadWriteTransaction) {
transaction.setObject(userCount, forKey: publicChatID, inCollection: "LokiPublicChatUserCountCollection")
transaction.setObject(userCount, forKey: publicChatID, inCollection: OWSPrimaryStorage.openGroupUserCountCollection)
}
}

View file

@ -22,7 +22,7 @@ public class LokiSessionResetImplementation : NSObject, SessionResetProtocol {
guard let preKeyMessage = whisperMessage as? PreKeyWhisperMessage else { return }
guard let storedPreKey = storage.getPreKeyRecord(forContact: recipientID, transaction: transaction) else {
print("[Loki] Received a friend request accepted message from a public key for which no pre key bundle was created.")
throw Errors.invalidPreKey
return // FIXME: This is causing trouble when it shouldn't...
}
guard storedPreKey.id == preKeyMessage.prekeyID else {
print("[Loki] Received a `PreKeyWhisperMessage` (friend request accepted message) from an unknown source.")

View file

@ -20,6 +20,9 @@ public extension Notification.Name {
public static let dataNukeRequested = Notification.Name("dataNukeRequested")
// Device linking
public static let unexpectedDeviceLinkRequestReceived = Notification.Name("unexpectedDeviceLinkRequestReceived")
// Onion requests
public static let buildingPaths = Notification.Name("buildingPaths")
public static let pathsBuilt = Notification.Name("pathsBuilt")
}
@objc public extension NSNotification {
@ -43,4 +46,7 @@ public extension Notification.Name {
@objc public static let dataNukeRequested = Notification.Name.dataNukeRequested.rawValue as NSString
// Device linking
@objc public static let unexpectedDeviceLinkRequestReceived = Notification.Name.unexpectedDeviceLinkRequestReceived.rawValue as NSString
// Onion requests
@objc public static let buildingPaths = Notification.Name.buildingPaths.rawValue as NSString
@objc public static let pathsBuilt = Notification.Name.pathsBuilt.rawValue as NSString
}

View file

@ -21,7 +21,7 @@ typedef NS_ENUM(NSInteger, TSWhisperMessageType) {
#define textSecureHTTPTimeOut 10
#define kLegalTermsUrlString @"https://getsession.org/legal/#privacy-policy"
#define kLegalTermsUrlString @"https://getsession.org/privacy-policy/"
//#ifndef DEBUG