This commit is contained in:
Niels Andriesse 2019-05-27 12:26:37 +10:00
parent 482721a2c2
commit 5351961af7
13 changed files with 108 additions and 160 deletions

View File

@ -320,12 +320,13 @@ static NSTimeInterval launchStartedAt;
if (self.lokiP2PServer.isRunning) { break; }
BOOL isStarted = [self.lokiP2PServer startOnPort:port.unsignedIntegerValue];
if (isStarted) {
NSURL *serverURL = self.lokiP2PServer.serverURL;
[LokiP2PManager setOurP2PAddressWithUrl:self.lokiP2PServer.serverURL];
NSString *serverURL = self.lokiP2PServer.serverURL.absoluteString;
if ([serverURL hasSuffix:@"/"]) {
serverURL = [serverURL substringToIndex:serverURL.length - 1];
NSString *serverURLDescription = serverURL.absoluteString;
if ([serverURLDescription hasSuffix:@"/"]) {
serverURLDescription = [serverURLDescription substringToIndex:serverURLDescription.length - 1];
}
OWSLogInfo(@"[Loki] Started server at %@.", serverURL);
OWSLogInfo(@"[Loki] Started server at %@.", serverURLDescription);
break;
}
}

View File

@ -9,15 +9,25 @@ public extension LokiAPI {
let data: LosslessStringConvertible
/// The time to live for the message in milliseconds.
let ttl: UInt64
/// Wether this message is a ping.
/// This should always be false unless it is from p2p pinging logic.
/// Whether this message is a ping.
///
/// - Note: The concept of pinging only applies to P2P messaging.
let isPing: Bool
/// When the proof of work was calculated, if applicable.
/// When the proof of work was calculated, if applicable (P2P messages don't require proof of work).
///
/// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970.
var timestamp: UInt64? = nil
/// The base 64 encoded proof of work, if applicable.
var nonce: String? = nil
private(set) var timestamp: UInt64?
/// The base 64 encoded proof of work, if applicable (P2P messages don't require proof of work).
private(set) var nonce: String?
private init(destination: String, data: LosslessStringConvertible, ttl: UInt64, isPing: Bool = false, timestamp: UInt64? = nil, nonce: String? = nil) {
self.destination = destination
self.data = data
self.ttl = ttl
self.isPing = isPing
self.timestamp = timestamp
self.nonce = nonce
}
/// Construct a `LokiMessage` from a `SignalMessage`.
///
@ -28,52 +38,28 @@ public extension LokiAPI {
let wrappedMessage = try LokiMessageWrapper.wrap(message: signalMessage, timestamp: timestamp)
let data = wrappedMessage.base64EncodedString()
let destination = signalMessage["destination"] as! String
var ttl = LokiAPI.defaultMessageTTL
if let messageTTL = signalMessage["ttl"] as? UInt, messageTTL > 0 { ttl = UInt64(messageTTL) }
if let messageTTL = signalMessage["ttl"] as! UInt?, messageTTL > 0 { ttl = UInt64(messageTTL) }
let isPing = signalMessage["isPing"] as! Bool
return Message(destination: destination, data: data, ttl: ttl, isPing: isPing)
} catch let error {
Logger.debug("[Loki] Failed to convert Signal message to Loki message: \(signalMessage)")
Logger.debug("[Loki] Failed to convert Signal message to Loki message: \(signalMessage).")
return nil
}
}
/// Create a basic loki message.
/// Calculate the proof of work for this message.
///
/// - Parameters:
/// - destination: The destination
/// - data: The data
/// - ttl: The time to live
public init(destination: String, data: LosslessStringConvertible, ttl: UInt64, isPing: Bool = false) {
self.destination = destination
self.data = data
self.ttl = ttl
self.isPing = isPing
}
/// Private init for setting proof of work. Use `calculatePoW` to get a message with these fields
private init(destination: String, data: LosslessStringConvertible, ttl: UInt64, isPing: Bool, timestamp: UInt64?, nonce: String?) {
self.destination = destination
self.data = data
self.ttl = ttl
self.isPing = isPing
self.timestamp = timestamp
self.nonce = nonce
}
/// Calculate the proof of work for this message
///
/// - Returns: This will return a promise with a new message which contains the proof of work
/// - Returns: The promise of a new message with its `timestamp` and `nonce` set.
public func calculatePoW() -> Promise<Message> {
// To match the desktop application, we have to wrap the data in an envelope and then wrap that in a websocket object
return Promise<Message> { seal in
DispatchQueue.global(qos: .default).async {
let now = NSDate.ows_millisecondTimeStamp()
if let nonce = ProofOfWork.calculate(data: self.data as! String, pubKey: self.destination, timestamp: now, ttl: self.ttl) {
let result = Message(destination: self.destination, data: self.data, ttl: self.ttl, isPing: self.isPing, timestamp: now, nonce: nonce)
let dataAsString = self.data as! String // Safe because of the way from(signalMessage:timestamp:) is implemented
if let nonce = ProofOfWork.calculate(data: dataAsString, pubKey: self.destination, timestamp: now, ttl: self.ttl) {
var result = self
result.timestamp = now
result.nonce = nonce
seal.fulfill(result)
} else {
seal.reject(Error.proofOfWorkCalculationFailed)

View File

@ -5,7 +5,7 @@ public extension LokiAPI {
// MARK: Settings
private static let minimumSnodeCount = 2 // TODO: For debugging purposes
private static let targetSnodeCount = 3 // TODO: For debugging purposes
private static let defaultSnodePort: UInt32 = 8080
private static let defaultSnodePort: UInt16 = 8080
// MARK: Caching
private static let swarmCacheKey = "swarmCacheKey"

View File

@ -3,9 +3,9 @@ internal extension LokiAPI {
internal struct Target : Hashable {
internal let address: String
internal let port: UInt32
internal let port: UInt16
internal init(address: String, port: UInt32) {
internal init(address: String, port: UInt16) {
self.address = address
self.port = port
}

View File

@ -5,10 +5,8 @@ import PromiseKit
// MARK: Settings
private static let version = "v1"
public static let defaultMessageTTL: UInt64 = 1 * 24 * 60 * 60 * 1000
private static let maxRetryCount: UInt = 3
private static let ourHexEncodedPubKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
public static let defaultMessageTTL: UInt64 = 1 * 24 * 60 * 60 * 1000
// MARK: Types
public typealias RawResponse = Any
@ -16,14 +14,12 @@ import PromiseKit
public enum Error : LocalizedError {
/// Only applicable to snode targets as proof of work isn't required for P2P messaging.
case proofOfWorkCalculationFailed
// Failed to send the message'
case internalError
case messageConversionFailed
public var errorDescription: String? {
switch self {
case .proofOfWorkCalculationFailed: return NSLocalizedString("Failed to calculate proof of work.", comment: "")
case .internalError: return "Failed while trying to send message"
case .messageConversionFailed: return "Failed to convert Signal message to Loki message."
}
}
}
@ -41,10 +37,11 @@ import PromiseKit
// MARK: Public API
public static func getMessages() -> Promise<Set<Promise<[SSKProtoEnvelope]>>> {
return getTargetSnodes(for: ourHexEncodedPubKey).mapValues { targetSnode in
let hexEncodedPublicKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
return getTargetSnodes(for: hexEncodedPublicKey).mapValues { targetSnode in
let lastHash = getLastMessageHashValue(for: targetSnode) ?? ""
let parameters: [String:Any] = [ "pubKey" : ourHexEncodedPubKey, "lastHash" : lastHash ]
return invoke(.getMessages, on: targetSnode, associatedWith: ourHexEncodedPubKey, parameters: parameters).map { rawResponse in
let parameters: [String:Any] = [ "pubKey" : hexEncodedPublicKey, "lastHash" : lastHash ]
return invoke(.getMessages, on: targetSnode, associatedWith: hexEncodedPublicKey, parameters: parameters).map { rawResponse in
guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] }
updateLastMessageHashValueIfPossible(for: targetSnode, from: rawMessages)
let newRawMessages = removeDuplicates(from: rawMessages)
@ -53,58 +50,45 @@ import PromiseKit
}.retryingIfNeeded(maxRetryCount: maxRetryCount).map { Set($0) }
}
public static func sendSignalMessage(_ signalMessage: SignalMessage, to destination: String, with timestamp: UInt64) -> Promise<Set<Promise<RawResponse>>> {
guard let lokiMessage = Message.from(signalMessage: signalMessage, timestamp: timestamp) else { return Promise(error: Error.messageConversionFailed) }
let destination = lokiMessage.destination
func sendLokiMessage(_ lokiMessage: Message, to targets: [Target]) -> Promise<Set<Promise<RawResponse>>> {
let parameters = lokiMessage.toJSON()
return Promise.value(targets).mapValues { invoke(.sendMessage, on: $0, associatedWith: destination, parameters: parameters) }.map { Set($0) }
}
func sendLokiMessageUsingSwarmAPI() -> Promise<Set<Promise<RawResponse>>> {
return lokiMessage.calculatePoW().then { updatedLokiMessage -> Promise<Set<Promise<RawResponse>>> in
return getTargetSnodes(for: destination).then { sendLokiMessage(updatedLokiMessage, to: $0) }
}
}
if let p2pDetails = LokiP2PManager.getDetails(forContact: destination), (lokiMessage.isPing || p2pDetails.isOnline) {
return sendLokiMessage(lokiMessage, to: [ p2pDetails.target ]).get { _ in
LokiP2PManager.setOnline(true, forContact: destination)
}.recover { error -> Promise<Set<Promise<RawResponse>>> in
LokiP2PManager.setOnline(false, forContact: destination)
if lokiMessage.isPing {
Logger.warn("[Loki] Failed to ping \(destination); marking contact as offline.")
if let nsError = error as? NSError {
nsError.isRetryable = false
throw nsError
} else {
throw error
}
}
return sendLokiMessageUsingSwarmAPI()
}
} else {
return sendLokiMessageUsingSwarmAPI()
}
}
// MARK: Public API (Obj-C)
@objc public static func objc_sendSignalMessage(_ signalMessage: SignalMessage, to destination: String, with timestamp: UInt64) -> AnyPromise {
let promise = sendSignalMessage(signalMessage, to: destination, timestamp: timestamp).mapValues { AnyPromise.from($0) }.map { Set($0) }
let promise = sendSignalMessage(signalMessage, to: destination, with: timestamp).mapValues { AnyPromise.from($0) }.map { Set($0) }
return AnyPromise.from(promise)
}
// MARK: Sending
public static func sendSignalMessage(_ signalMessage: SignalMessage, to destination: String, timestamp: UInt64) -> Promise<Set<Promise<RawResponse>>> {
guard let message = Message.from(signalMessage: signalMessage, timestamp: timestamp) else {
return Promise(error: Error.internalError)
}
// Send message through the storage server
// We put this here because `recover` expects `Promise<Set<Promise<RawResponse>>>`
let sendThroughStorageServer: () -> Promise<Set<Promise<RawResponse>>> = { () in
return message.calculatePoW().then { powMessage -> Promise<Set<Promise<RawResponse>>> in
let snodes = getTargetSnodes(for: powMessage.destination)
return sendMessage(powMessage, targets: snodes)
}
}
// If we have the p2p details and we have marked the user as online OR we are pinging the user, then use peer to peer
// If that failes then fallback to storage server
if let p2pDetails = LokiP2PManager.getDetails(forContact: destination), message.isPing || p2pDetails.isOnline {
let targets = Promise.wrap([p2pDetails.target])
return sendMessage(message, targets: targets).then { result -> Promise<Set<Promise<RawResponse>>> in
LokiP2PManager.setOnline(true, forContact: destination)
return Promise.wrap(result)
}.recover { error -> Promise<Set<Promise<RawResponse>>> in
// The user is not online
LokiP2PManager.setOnline(false, forContact: destination)
// If it was a ping then don't send to the storage server
if (message.isPing) {
Logger.warn("[Loki] Failed to ping \(destination) - Marking contact as offline.")
let nserror = error as NSError
nserror.isRetryable = false
throw nserror
}
return sendThroughStorageServer()
}
}
return sendThroughStorageServer()
}
internal static func sendMessage(_ lokiMessage: Message, targets: Promise<[Target]>) -> Promise<Set<Promise<RawResponse>>> {
let parameters = lokiMessage.toJSON()
return targets.mapValues { invoke(.sendMessage, on: $0, associatedWith: lokiMessage.destination, parameters: parameters) }.map { Set($0) }
}
// MARK: Parsing
// The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions.

View File

@ -1,7 +1,7 @@
@objc public class LokiP2PManager : NSObject {
private static let storage = OWSPrimaryStorage.shared()
private static let messageSender: MessageSender = SSKEnvironment.shared.messageSender
private static let messageSender = SSKEnvironment.shared.messageSender
private static let ourHexEncodedPubKey = OWSIdentityManager.shared().identityKeyPair()!.hexEncodedPublicKey
/// The amount of time before pinging when a user is set to offline
@ -10,7 +10,7 @@
/// A p2p state struct
internal struct P2PDetails {
var address: String
var port: UInt32
var port: UInt16
var isOnline: Bool
var timerDuration: Double
var pingTimer: Timer? = nil
@ -33,7 +33,7 @@
/// - Parameter url: The url to our local server
@objc public static func setOurP2PAddress(url: URL) {
guard let scheme = url.scheme, let host = url.host, let port = url.port else { return }
let target = LokiAPI.Target(address: "\(scheme)://\(host)", port: UInt32(port))
let target = LokiAPI.Target(address: "\(scheme)://\(host)", port: UInt16(port))
ourP2PAddress = target
}
@ -50,12 +50,12 @@
}
guard let thread = contactThread else {
Logger.warn("[Loki][Ping] Failed to fetch thread for \(pubKey)")
Logger.warn("[Loki][Ping] Failed to fetch thread for \(pubKey).")
return
}
guard let message = lokiAddressMessage(for: thread, isPing: true) else {
Logger.warn("[Loki][Ping] Failed to build ping message for \(pubKey)")
Logger.warn("[Loki][Ping] Failed to build ping message for \(pubKey).")
return
}
@ -63,7 +63,7 @@
}
}
/// Broadcash an online message to all our friends.
/// Broadcast an online message to all our friends.
/// This shouldn't be called inside a transaction.
@objc public static func broadcastOnlineStatus() {
// Escape any transaction blocks
@ -100,7 +100,7 @@
/// - address: The pther users p2p address
/// - port: The other users p2p port
/// - receivedThroughP2P: Wether we received the message through p2p
@objc internal static func didReceiveLokiAddressMessage(forContact pubKey: String, address: String, port: UInt32, receivedThroughP2P: Bool) {
@objc internal static func didReceiveLokiAddressMessage(forContact pubKey: String, address: String, port: UInt16, receivedThroughP2P: Bool) {
// Stagger the ping timers so that contacts don't ping each other at the same time
let timerDuration = pubKey < ourHexEncodedPubKey ? 1 * kMinuteInterval : 2 * kMinuteInterval
@ -127,7 +127,7 @@
*/
if oldContactExists && receivedThroughP2P && wasOnline && p2pDetailsMatch {
setOnline(true, forContact: pubKey)
return;
return
}
/*
@ -174,7 +174,7 @@
messageSender.sendPromise(message: message).catch { error in
Logger.warn("Failed to send online status to \(thread.contactIdentifier())")
}.retainUntilComplete()
}.retainUntilComplete()
}
private static func getAllFriendThreads() -> [TSContactThread] {

View File

@ -1,7 +1,7 @@
@objc internal final class TargetWrapper : NSObject, NSCoding {
internal let address: String
internal let port: UInt32
internal let port: UInt16
internal init(from target: LokiAPI.Target) {
address = target.address
@ -11,7 +11,7 @@
internal init?(coder: NSCoder) {
address = coder.decodeObject(forKey: "address") as! String
port = coder.decodeObject(forKey: "port") as! UInt32
port = coder.decodeObject(forKey: "port") as! UInt16
super.init()
}

View File

@ -1,4 +1,3 @@
#import "LKEphemeralMessage.h"
NS_ASSUME_NONNULL_BEGIN
@ -6,15 +5,12 @@ NS_ASSUME_NONNULL_BEGIN
NS_SWIFT_NAME(LokiAddressMessage)
@interface LKAddressMessage : LKEphemeralMessage
- (instancetype)initInThread:(nullable TSThread *)thread
address:(NSString *)address
port:(uint)port
isPing:(BOOL)isPing;
@property (nonatomic, readonly) NSString *address;
@property (nonatomic, readonly) uint port;
@property (nonatomic, readonly) uint16_t port;
@property (nonatomic, readonly) BOOL isPing;
- (instancetype)initInThread:(nullable TSThread *)thread address:(NSString *)address port:(uint16_t)port isPing:(BOOL)isPing;
@end
NS_ASSUME_NONNULL_END

View File

@ -6,42 +6,36 @@
@interface LKAddressMessage ()
@property (nonatomic) NSString *address;
@property (nonatomic) uint port;
@property (nonatomic) uint16_t port;
@property (nonatomic) BOOL isPing;
@end
@implementation LKAddressMessage
- (instancetype)initInThread:(nullable TSThread *)thread
address:(NSString *)address
port:(uint)port
isPing:(bool)isPing
- (instancetype)initInThread:(nullable TSThread *)thread address:(NSString *)address port:(uint16_t)port isPing:(bool)isPing
{
self = [super initInThread:thread];
if (!self) {
return self;
self = [LKEphemeralMessage createEmptyOutgoingMessageInThread:thread];
if (self) {
_address = address;
_port = port;
_isPing = isPing;
}
_address = address;
_port = port;
_isPing = isPing;
return self;
}
- (SSKProtoContentBuilder *)contentBuilder:(SignalRecipient *)recipient {
SSKProtoContentBuilder *contentBuilder = [super contentBuilder:recipient];
// Se
SSKProtoLokiAddressMessageBuilder *addressBuilder = SSKProtoLokiAddressMessage.builder;
SSKProtoLokiAddressMessageBuilder *addressBuilder = [SSKProtoLokiAddressMessage builder];
[addressBuilder setPtpAddress:self.address];
[addressBuilder setPtpPort:self.port];
uint32_t portAsUInt32 = self.port;
[addressBuilder setPtpPort:portAsUInt32];
NSError *error;
SSKProtoLokiAddressMessage *addressMessage = [addressBuilder buildAndReturnError:&error];
if (error || !addressMessage) {
OWSFailDebug(@"Failed to build lokiAddressMessage for %@: %@", recipient.recipientId, error);
OWSFailDebug(@"Failed to build LokiAddressMessage for %@: %@.", recipient.recipientId, error);
} else {
[contentBuilder setLokiAddressMessage:addressMessage];
}

View File

@ -5,11 +5,9 @@ NS_ASSUME_NONNULL_BEGIN
NS_SWIFT_NAME(EphemeralMessage)
@interface LKEphemeralMessage : TSOutgoingMessage
/// Used to establish sessions.
/// Used for e.g. session initialization.
+ (LKEphemeralMessage *)createEmptyOutgoingMessageInThread:(TSThread *)thread;
- (instancetype)initInThread:(nullable TSThread *)thread;
@end
NS_ASSUME_NONNULL_END

View File

@ -4,12 +4,8 @@
@implementation LKEphemeralMessage
+ (LKEphemeralMessage *)createEmptyOutgoingMessageInThread:(TSThread *)thread {
return [[LKEphemeralMessage alloc] initInThread:thread];
}
- (instancetype)initInThread:(nullable TSThread *)thread {
return [self initOutgoingMessageWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:thread messageBody:@"" attachmentIds:[NSMutableArray<NSString *> new]
expiresInSeconds:0 expireStartedAt:0 isVoiceMessage:NO groupMetaMessage:TSGroupMetaMessageUnspecified quotedMessage:nil contactShare:nil linkPreview:nil];
return [[LKEphemeralMessage alloc] initOutgoingMessageWithTimestamp:NSDate.ows_millisecondTimeStamp inThread:thread messageBody:@"" attachmentIds:[NSMutableArray<NSString *> new]
expiresInSeconds:0 expireStartedAt:0 isVoiceMessage:NO groupMetaMessage:TSGroupMetaMessageUnspecified quotedMessage:nil contactShare:nil linkPreview:nil];
}
- (BOOL)shouldSyncTranscript { return NO; }

View File

@ -1,9 +0,0 @@
import PromiseKit
public extension Promise {
static func wrap(_ value: T) -> Promise<T> {
return Promise<T> { resolver in
resolver.fulfill(value)
}
}
}

View File

@ -496,9 +496,9 @@ dispatch_queue_t NetworkManagerQueue()
+ (void)deregisterAfterAuthErrorIfNecessary:(NSURLSessionDataTask *)task
request:(TSRequest *)request
statusCode:(NSInteger)statusCode {
/* Loki Original Code
* - We don't really care about invalid auth
* -------------
/* Loki: Original code
* We don't really care about invalid auth
* ========
OWSLogVerbose(@"Invalid auth: %@", task.originalRequest.allHTTPHeaderFields);
@ -526,7 +526,9 @@ dispatch_queue_t NetworkManagerQueue()
} else {
OWSLogWarn(@"Ignoring %d for URL: %@", (int)statusCode, task.originalRequest.URL.absoluteString);
}
*/
* ========
*/
}
+ (NSError *)errorWithHTTPCode:(NSInteger)code