2019-09-26 03:32:47 +02:00
import PromiseKit
2020-02-10 04:40:53 +01:00
// / B a s e c l a s s f o r ` L o k i F i l e S e r v e r A P I ` a n d ` L o k i P u b l i c C h a t A P I ` .
2019-09-26 03:32:47 +02:00
public class LokiDotNetAPI : NSObject {
// MARK: C o n v e n i e n c e
internal static let storage = OWSPrimaryStorage . shared ( )
2019-10-02 05:34:34 +02:00
internal static let userKeyPair = OWSIdentityManager . shared ( ) . identityKeyPair ( ) !
2019-09-26 03:32:47 +02:00
internal static let userHexEncodedPublicKey = userKeyPair . hexEncodedPublicKey
2019-10-18 00:46:44 +02:00
// MARK: S e t t i n g s
private static let attachmentType = " network.loki "
2019-09-26 03:32:47 +02:00
// MARK: E r r o r
2020-02-14 00:16:53 +01:00
@objc public class LokiDotNetAPIError : NSError { // N o t c a l l e d ` E r r o r ` f o r O b j - C i n t e r o p e r a b l i t y
2020-02-10 05:47:15 +01:00
2020-02-14 00:16:53 +01:00
@objc public static let generic = LokiDotNetAPIError ( domain : " LokiDotNetAPIErrorDomain " , code : 1 , userInfo : [ NSLocalizedDescriptionKey : " An error occurred. " ] )
@objc public static let parsingFailed = LokiDotNetAPIError ( domain : " LokiDotNetAPIErrorDomain " , code : 2 , userInfo : [ NSLocalizedDescriptionKey : " Invalid file server response. " ] )
@objc public static let signingFailed = LokiDotNetAPIError ( domain : " LokiDotNetAPIErrorDomain " , code : 3 , userInfo : [ NSLocalizedDescriptionKey : " Couldn't sign message. " ] )
@objc public static let encryptionFailed = LokiDotNetAPIError ( domain : " LokiDotNetAPIErrorDomain " , code : 4 , userInfo : [ NSLocalizedDescriptionKey : " Couldn't encrypt file. " ] )
@objc public static let decryptionFailed = LokiDotNetAPIError ( domain : " LokiDotNetAPIErrorDomain " , code : 5 , userInfo : [ NSLocalizedDescriptionKey : " Couldn't decrypt file. " ] )
@objc public static let maxFileSizeExceeded = LokiDotNetAPIError ( domain : " LokiDotNetAPIErrorDomain " , code : 6 , userInfo : [ NSLocalizedDescriptionKey : " Maximum file size exceeded. " ] )
2019-09-26 03:32:47 +02:00
}
// MARK: D a t a b a s e
// / T o b e o v e r r i d d e n b y s u b c l a s s e s .
internal class var authTokenCollection : String { preconditionFailure ( " authTokenCollection is abstract and must be overridden. " ) }
private static func getAuthTokenFromDatabase ( for server : String ) -> String ? {
var result : String ? = nil
storage . dbReadConnection . read { transaction in
result = transaction . object ( forKey : server , inCollection : authTokenCollection ) as ! String ?
}
return result
}
private static func setAuthToken ( for server : String , to newValue : String ) {
storage . dbReadWriteConnection . readWrite { transaction in
transaction . setObject ( newValue , forKey : server , inCollection : authTokenCollection )
}
}
// MARK: L i f e c y c l e
override private init ( ) { }
2019-10-18 00:46:44 +02:00
// MARK: A t t a c h m e n t s ( P u b l i c A P I )
public static func uploadAttachment ( _ attachment : TSAttachmentStream , with attachmentID : String , to server : String ) -> Promise < Void > {
2020-02-10 04:40:53 +01:00
let isEncryptionRequired = ( server = = LokiFileServerAPI . server )
2019-10-18 00:46:44 +02:00
return Promise < Void > ( ) { seal in
2020-02-10 04:38:33 +01:00
func proceed ( with token : String ) {
2020-02-10 02:01:23 +01:00
// G e t t h e a t t a c h m e n t
2019-10-18 02:33:14 +02:00
let data : Data
2019-10-18 00:46:44 +02:00
guard let unencryptedAttachmentData = try ? attachment . readDataFromFile ( ) else {
2020-02-10 02:01:23 +01:00
print ( " [Loki] Couldn't read attachment from disk. " )
2020-02-14 00:16:53 +01:00
return seal . reject ( LokiDotNetAPIError . generic )
2019-10-18 00:46:44 +02:00
}
2019-10-18 02:33:14 +02:00
// E n c r y p t t h e a t t a c h m e n t i f n e e d e d
if isEncryptionRequired {
var encryptionKey = NSData ( )
var digest = NSData ( )
guard let encryptedAttachmentData = Cryptography . encryptAttachmentData ( unencryptedAttachmentData , outKey : & encryptionKey , outDigest : & digest ) else {
print ( " [Loki] Couldn't encrypt attachment. " )
2020-02-14 00:16:53 +01:00
return seal . reject ( LokiDotNetAPIError . encryptionFailed )
2019-10-18 02:33:14 +02:00
}
attachment . encryptionKey = encryptionKey as Data
attachment . digest = digest as Data
data = encryptedAttachmentData
} else {
data = unencryptedAttachmentData
2019-10-18 00:46:44 +02:00
}
2020-02-10 05:47:15 +01:00
// C h e c k t h e f i l e s i z e i f n e e d e d
let isLokiFileServer = ( server = = LokiFileServerAPI . server )
if isLokiFileServer && data . count > LokiFileServerAPI . maxFileSize {
2020-02-14 00:16:53 +01:00
return seal . reject ( LokiDotNetAPIError . maxFileSizeExceeded )
2020-02-10 05:47:15 +01:00
}
2019-10-18 00:46:44 +02:00
// C r e a t e t h e r e q u e s t
let url = " \( server ) /files "
let parameters : JSON = [ " type " : attachmentType , " Content-Type " : " application/binary " ]
var error : NSError ?
var request = AFHTTPRequestSerializer ( ) . multipartFormRequest ( withMethod : " POST " , urlString : url , parameters : parameters , constructingBodyWith : { formData in
2019-10-18 02:33:14 +02:00
formData . appendPart ( withFileData : data , name : " content " , fileName : UUID ( ) . uuidString , mimeType : " application/binary " )
2019-10-18 00:46:44 +02:00
} , error : & error )
2020-02-10 04:38:33 +01:00
request . addValue ( " Bearer \( token ) " , forHTTPHeaderField : " Authorization " )
2019-10-18 00:46:44 +02:00
if let error = error {
print ( " [Loki] Couldn't upload attachment due to error: \( error ) . " )
2020-02-10 02:01:23 +01:00
return seal . reject ( error )
2019-10-18 00:46:44 +02:00
}
// S e n d t h e r e q u e s t
2020-02-03 10:55:42 +01:00
func parseResponse ( _ response : Any ) {
// P a r s e t h e s e r v e r I D & d o w n l o a d U R L
guard let json = response as ? JSON , let data = json [ " data " ] as ? JSON , let serverID = data [ " id " ] as ? UInt64 , let downloadURL = data [ " url " ] as ? String else {
print ( " [Loki] Couldn't parse attachment from: \( response ) . " )
2020-02-14 00:16:53 +01:00
return seal . reject ( LokiDotNetAPIError . parsingFailed )
2020-02-03 10:55:42 +01:00
}
// U p d a t e t h e a t t a c h m e n t
attachment . serverId = serverID
attachment . isUploaded = true
attachment . downloadURL = downloadURL
attachment . save ( )
seal . fulfill ( ( ) )
}
2020-02-10 04:40:53 +01:00
let isProxyingRequired = ( server = = LokiFileServerAPI . server ) // D o n ' t p r o x y o p e n g r o u p r e q u e s t s f o r n o w
2020-02-03 10:27:13 +01:00
if isProxyingRequired {
2020-02-10 06:27:10 +01:00
attachment . isUploaded = false
attachment . save ( )
2020-02-03 10:27:13 +01:00
let _ = LokiFileServerProxy ( for : server ) . performLokiFileServerNSURLRequest ( request as NSURLRequest ) . done { responseObject in
2020-02-03 10:55:42 +01:00
parseResponse ( responseObject )
2020-02-03 06:50:14 +01:00
} . catch { error in
seal . reject ( error )
2019-10-18 00:46:44 +02:00
}
2020-02-03 06:50:14 +01:00
} else {
let task = AFURLSessionManager ( sessionConfiguration : . default ) . uploadTask ( withStreamedRequest : request as URLRequest , progress : { rawProgress in
// B r o a d c a s t p r o g r e s s u p d a t e s
let progress = max ( 0.1 , rawProgress . fractionCompleted )
let userInfo : [ String : Any ] = [ kAttachmentUploadProgressKey : progress , kAttachmentUploadAttachmentIDKey : attachmentID ]
DispatchQueue . main . async {
NotificationCenter . default . post ( name : . attachmentUploadProgress , object : nil , userInfo : userInfo )
}
} , completionHandler : { response , responseObject , error in
if let error = error {
print ( " [Loki] Couldn't upload attachment due to error: \( error ) . " )
return seal . reject ( error )
}
let statusCode = ( response as ! HTTPURLResponse ) . statusCode
let isSuccessful = ( 200. . . 299 ) ~= statusCode
guard isSuccessful else {
print ( " [Loki] Couldn't upload attachment. " )
2020-02-14 00:16:53 +01:00
return seal . reject ( LokiDotNetAPIError . generic )
2020-02-03 06:50:14 +01:00
}
2020-02-03 10:55:42 +01:00
parseResponse ( responseObject )
2020-02-03 06:50:14 +01:00
} )
task . resume ( )
}
2020-02-10 02:01:23 +01:00
}
2020-02-10 04:40:53 +01:00
if server = = LokiFileServerAPI . server {
2020-02-10 04:38:33 +01:00
proceed ( with : " loki " ) // U p l o a d s t o t h e L o k i F i l e S e r v e r s h o u l d n ' t i n c l u d e a n y p e r s o n a l l y i d e n t i f i a b l e i n f o r m a t i o n s o u s e a d u m m y a u t h t o k e n
2020-02-10 02:01:23 +01:00
} else {
getAuthToken ( for : server ) . done ( on : DispatchQueue . global ( ) ) { token in
proceed ( with : token )
} . catch ( on : DispatchQueue . global ( ) ) { error in
print ( " [Loki] Couldn't upload attachment due to error: \( error ) . " )
seal . reject ( error )
}
2019-10-18 00:46:44 +02:00
}
}
}
2019-09-26 03:32:47 +02:00
// MARK: I n t e r n a l A P I
internal static func getAuthToken ( for server : String ) -> Promise < String > {
if let token = getAuthTokenFromDatabase ( for : server ) {
return Promise . value ( token )
} else {
2019-11-13 03:12:25 +01:00
return requestNewAuthToken ( for : server ) . then ( on : DispatchQueue . global ( ) ) { submitAuthToken ( $0 , for : server ) } . map { token -> String in
2019-09-26 03:32:47 +02:00
setAuthToken ( for : server , to : token )
return token
}
}
}
// MARK: P r i v a t e A P I
private static func requestNewAuthToken ( for server : String ) -> Promise < String > {
print ( " [Loki] Requesting auth token for server: \( server ) . " )
let queryParameters = " pubKey= \( userHexEncodedPublicKey ) "
let url = URL ( string : " \( server ) /loki/v1/get_challenge? \( queryParameters ) " ) !
let request = TSRequest ( url : url )
2020-02-02 01:33:34 +01:00
return LokiFileServerProxy ( for : server ) . perform ( request , withCompletionQueue : DispatchQueue . global ( ) ) . map { rawResponse in
2019-09-26 03:32:47 +02:00
guard let json = rawResponse as ? JSON , let base64EncodedChallenge = json [ " cipherText64 " ] as ? String , let base64EncodedServerPublicKey = json [ " serverPubKey64 " ] as ? String ,
let challenge = Data ( base64Encoded : base64EncodedChallenge ) , var serverPublicKey = Data ( base64Encoded : base64EncodedServerPublicKey ) else {
2020-02-14 00:16:53 +01:00
throw LokiDotNetAPIError . parsingFailed
2019-09-26 03:32:47 +02:00
}
// D i s c a r d t h e " 0 5 " p r e f i x i f n e e d e d
2020-02-10 02:01:23 +01:00
if serverPublicKey . count = = 33 {
2019-10-02 05:34:34 +02:00
let hexEncodedServerPublicKey = serverPublicKey . toHexString ( )
2019-09-26 03:32:47 +02:00
serverPublicKey = Data . data ( fromHex : hexEncodedServerPublicKey . substring ( from : 2 ) ) !
}
// T h e c h a l l e n g e i s p r e f i x e d b y t h e 1 6 b i t I V
guard let tokenAsData = try ? DiffieHellman . decrypt ( challenge , publicKey : serverPublicKey , privateKey : userKeyPair . privateKey ) ,
let token = String ( bytes : tokenAsData , encoding : . utf8 ) else {
2020-02-14 00:16:53 +01:00
throw LokiDotNetAPIError . decryptionFailed
2019-09-26 03:32:47 +02:00
}
return token
}
}
private static func submitAuthToken ( _ token : String , for server : String ) -> Promise < String > {
print ( " [Loki] Submitting auth token for server: \( server ) . " )
let url = URL ( string : " \( server ) /loki/v1/submit_challenge " ) !
let parameters = [ " pubKey " : userHexEncodedPublicKey , " token " : token ]
let request = TSRequest ( url : url , method : " POST " , parameters : parameters )
2020-02-02 01:33:34 +01:00
return LokiFileServerProxy ( for : server ) . perform ( request , withCompletionQueue : DispatchQueue . global ( ) ) . map { _ in token }
2019-09-26 03:32:47 +02:00
}
2019-10-18 00:46:44 +02:00
// MARK: A t t a c h m e n t s ( P u b l i c O b j - C A P I )
@objc ( uploadAttachment : withID : toServer : )
public static func objc_uploadAttachment ( _ attachment : TSAttachmentStream , with attachmentID : String , to server : String ) -> AnyPromise {
return AnyPromise . from ( uploadAttachment ( attachment , with : attachmentID , to : server ) )
}
2019-09-26 03:32:47 +02:00
}