Statistics
| Branch: | Tag: | Revision:

chatsecureios / ChatSecure / Classes / Controllers / FileTransferManager.swift @ c6c7b30a

History | View | Annotate | Download (38.1 KB)

1
//
2
//  FileTransferManager.swift
3
//  ChatSecure
4
//
5
//  Created by Chris Ballinger on 3/28/17.
6
//  Copyright © 2017 Chris Ballinger. All rights reserved.
7
//
8

    
9
import Foundation
10
import XMPPFramework
11
import CocoaLumberjack
12
import OTRKit
13
import Alamofire
14
import OTRAssets
15

    
16
extension UIImage {
17
    enum DataSize {
18
        case unlimited
19
        case maxBytes(UInt)
20
        var numBytes: UInt {
21
            switch self {
22
            case .unlimited:
23
                return UInt.max
24
            case .maxBytes(let limit):
25
                return limit
26
            }
27
        }
28
    }
29
     struct Quality {
30
        static let low = Quality(initial: 0.4, decrementFactor: 0.65)
31
        static let medium = Quality(initial: 0.65, decrementFactor: 0.65)
32
        static let high = Quality(initial: 0.75, decrementFactor: 0.75)
33
        
34
        /// This value cannot be > 1 or bad things will happen
35
        let initial: CGFloat
36
        /// Multiplied to reduce the initial value. This value cannot be > 1 or bad things will happen
37
        let decrementFactor: CGFloat
38
    }
39
    func jpegData(dataSize: DataSize,
40
                  resize: Quality = Quality.medium,
41
                  jpeg: Quality = Quality.medium,
42
                  maxTries: UInt = 10) -> Data? {
43
        let image = self
44
        var sizeInBytes: UInt = 0
45
        var scaleFactor: CGFloat = resize.initial
46
        var jpegQuality: CGFloat = jpeg.initial
47
        let qualityDecrement: CGFloat = jpeg.decrementFactor
48
        let scaleDecrement: CGFloat = resize.decrementFactor
49
        var scaledImageData: Data? = nil
50
        var newSize = CGSize.zero
51
        let maxTries = 10
52
        var numTries = 0
53
        let maxSize = dataSize.numBytes
54
        while (sizeInBytes == 0 || sizeInBytes > maxSize) &&
55
            numTries < maxTries {
56
                numTries = numTries + 1
57
                newSize = CGSize(width: image.size.width * scaleFactor, height: image.size.height * scaleFactor)
58
                let scaledImage = UIImage.otr_image(with: image, scaledTo: newSize)
59
                scaledImageData = UIImageJPEGRepresentation(scaledImage, jpegQuality)
60
                if let imageData = scaledImageData {
61
                    sizeInBytes = UInt(imageData.count)
62
                    scaleFactor = scaleFactor * scaleDecrement
63
                    jpegQuality = jpegQuality * qualityDecrement
64
                } else {
65
                    DDLogError("Could not make JPEG out of image!")
66
                    return nil
67
                }
68
        }
69
        return scaledImageData
70
    }
71
}
72

    
73
public enum FileTransferError: CustomNSError {
74
    case unknown
75
    case noServers
76
    case serverError
77
    case exceedsMaxSize
78
    case urlFormatting
79
    case fileNotFound
80
    case keyGenerationError
81
    case cryptoError
82
    case automaticDownloadsDisabled
83
    
84
    public var localizedDescription: String {
85
        switch self {
86
        case .unknown:
87
            return UNKNOWN_ERROR_STRING()
88
        case .noServers:
89
            return NO_HTTP_UPLOAD_SERVERS_STRING() + " " + PLEASE_CONTACT_SERVER_OP_STRING()
90
        case .serverError:
91
            return UNKNOWN_ERROR_STRING() + " " + PLEASE_CONTACT_SERVER_OP_STRING()
92
        case .exceedsMaxSize:
93
            return FILE_EXCEEDS_MAX_SIZE_STRING()
94
        case .urlFormatting:
95
            return COULD_NOT_PARSE_URL_STRING()
96
        case .fileNotFound:
97
            return FILE_NOT_FOUND_STRING()
98
        case .cryptoError, .keyGenerationError:
99
            return errSSLCryptoString()
100
        case .automaticDownloadsDisabled:
101
            return AUTOMATIC_DOWNLOADS_DISABLED_STRING()
102
        }
103
    }
104
}
105

    
106
public class FileTransferManager: NSObject, OTRServerCapabilitiesDelegate {
107

    
108
    let httpFileUpload: XMPPHTTPFileUpload
109
    let serverCapabilities: OTRServerCapabilities
110
    let connection: YapDatabaseConnection
111
    let internalQueue = DispatchQueue(label: "FileTransferManager Queue")
112
    let callbackQueue = DispatchQueue.main
113
    let sessionManager: SessionManager
114
    private var servers: [HTTPServer] = []
115
    
116
    @objc public var canUploadFiles: Bool {
117
        return self.servers.first != nil
118
    }
119
    
120
    deinit {
121
        httpFileUpload.removeDelegate(self)
122
        serverCapabilities.removeDelegate(self)
123
    }
124
    
125
    @objc public init(connection: YapDatabaseConnection,
126
                serverCapabilities: OTRServerCapabilities,
127
                sessionConfiguration: URLSessionConfiguration) {
128
        self.serverCapabilities = serverCapabilities
129
        self.httpFileUpload = XMPPHTTPFileUpload()
130
        self.connection = connection
131
        self.sessionManager = Alamofire.SessionManager(configuration: sessionConfiguration)
132
        super.init()
133
        if let stream = serverCapabilities.xmppStream {
134
            httpFileUpload.activate(stream)
135
        }
136
        httpFileUpload.addDelegate(self, delegateQueue: DispatchQueue.main)
137
        serverCapabilities.addDelegate(self, delegateQueue: DispatchQueue.main)
138
        self.refreshCapabilities()
139
    }
140
    
141
    // MARK: - Public Methods
142
    
143
    /// This will fetch capabilities and setup XMPP transfer module if needed
144
    @objc public func refreshCapabilities() {
145
        guard let allCapabilities = serverCapabilities.allCapabilities else {
146
            serverCapabilities.fetchAllCapabilities()
147
            return
148
        }
149
        servers = serversFromCapabilities(capabilities: allCapabilities)
150
        serverCapabilities.fetchAllCapabilities()
151
    }
152

    
153
    private func upload(mediaItem: OTRMediaItem,
154
                        shouldEncrypt: Bool,
155
                       prefetchedData: Data?,
156
                       completion: @escaping (_ url: URL?, _ error: Error?) -> ()) {
157
        internalQueue.async {
158
            if let data = prefetchedData {
159
                self.upload(media: mediaItem, data: data, shouldEncrypt: shouldEncrypt, filename: mediaItem.filename, contentType: mediaItem.mimeType, completion: completion)
160
            } else {
161
                var url: URL? = nil
162
                self.connection.read({ (transaction) in
163
                    url = mediaItem.mediaServerURL(with: transaction)
164
                })
165
                if let url = url {
166
                    self.upload(media: mediaItem, file: url, shouldEncrypt: shouldEncrypt, completion: completion)
167
                } else {
168
                    let error = FileTransferError.fileNotFound
169
                    DDLogError("Upload filed: File not found \(error)")
170
                    self.callbackQueue.async {
171
                        completion(nil, error)
172
                    }
173
                }
174
            }
175
        }
176
    }
177
    
178
    /// Currently just a wrapper around sendData
179
    private func upload(media: OTRMediaItem,
180
                        file: URL,
181
                        shouldEncrypt: Bool,
182
                     completion: @escaping (_ url: URL?, _ error: Error?) -> ()) {
183
        internalQueue.async {
184
            do {
185
                let data = try Data(contentsOf: file)
186
                let mimeType = OTRKitGetMimeTypeForExtension(file.pathExtension)
187
                self.upload(media: media, data: data, shouldEncrypt: shouldEncrypt, filename: file.lastPathComponent, contentType: mimeType, completion: completion)
188
            } catch let error {
189
                DDLogError("Error sending file URL \(file): \(error)")
190
            }
191
        }
192
        
193
    }
194
    
195
    private func upload(media: OTRMediaItem,
196
                        data inData: Data,
197
                        shouldEncrypt: Bool,
198
                 filename: String,
199
                 contentType: String,
200
                 completion: @escaping (_ url: URL?, _ error: Error?) -> ()) {
201
        internalQueue.async {
202
            guard let service = self.servers.first else {
203
                DDLogWarn("No HTTP upload servers available")
204
                self.callbackQueue.async {
205
                    completion(nil, FileTransferError.noServers)
206
                }
207
                return
208
            }
209
            var data = inData
210
            
211
            // When resending images, sometimes we need to recompress them
212
            // to fit the max upload limit
213
            if UInt(data.count) > service.maxSize,
214
                let _ = media as? OTRImageItem,
215
                let image = UIImage(data: inData),
216
                let imageData = image.jpegData(dataSize: .maxBytes(service.maxSize), resize: UIImage.Quality.medium, jpeg: UIImage.Quality.medium, maxTries: 10)
217
                {
218
                    data = imageData
219
            }
220
            
221
            if UInt(data.count) > service.maxSize {
222
                DDLogError("HTTP Upload exceeds max size \(data.count) > \(service.maxSize)")
223
                self.callbackQueue.async {
224
                    completion(nil, FileTransferError.exceedsMaxSize)
225
                }
226
                return
227
            }
228
            
229
            // TODO: Refactor to use streaming encryption
230
            var outData = data
231
            var outKeyIv: Data? = nil
232
            if shouldEncrypt {
233
                guard let key = OTRPasswordGenerator.randomData(withLength: 32), let iv = OTRPasswordGenerator.randomData(withLength: 16) else {
234
                    DDLogError("Could not generate key/iv")
235
                    self.callbackQueue.async {
236
                        completion(nil, FileTransferError.keyGenerationError)
237
                    }
238
                    return
239
                }
240
                outKeyIv = iv + key
241
                do {
242
                    let crypted = try OTRCryptoUtility.encryptAESGCMData(data, key: key, iv: iv)
243
                    outData = crypted.data + crypted.authTag
244
                } catch let error {
245
                    outData = Data()
246
                    DDLogError("Could not encrypt data for file transfer \(error)")
247
                    self.callbackQueue.async {
248
                        completion(nil, error)
249
                    }
250
                    return
251
                }
252
            }
253
            
254
            
255
            self.httpFileUpload.requestSlot(fromService: service.jid, filename: filename, size: UInt(outData.count), contentType: contentType, completion: { (slot: XMPPSlot?, iq: XMPPIQ?, error: Error?) in
256
                guard let slot = slot else {
257
                    let outError = error ?? FileTransferError.serverError
258
                    DDLogError("\(service) failed to assign upload slot: \(outError)")
259
                    self.callbackQueue.async {
260
                        completion(nil, outError)
261
                    }
262
                    return
263
                }
264
                self.sessionManager.upload(outData, to: slot.putURL, method: .put)
265
                    .validate()
266
                    .responseData(queue: self.callbackQueue) { response in
267
                        switch response.result {
268
                        case .success:
269
                            if let outKeyIv = outKeyIv {
270
                                // If there's a AES-GCM key, we gotta put it in the url
271
                                // and change the scheme to `aesgcm`
272
                                if var components = URLComponents(url: slot.getURL, resolvingAgainstBaseURL: true) {
273
                                    components.scheme = URLScheme.aesgcm.rawValue
274
                                    components.fragment = outKeyIv.hexString()
275
                                    if let outURL = components.url {
276
                                        completion(outURL, nil)
277
                                    } else {
278
                                        completion(nil, FileTransferError.urlFormatting)
279
                                    }
280
                                } else {
281
                                    completion(nil, FileTransferError.urlFormatting)
282
                                }
283
                            } else {
284
                                // The plaintext case
285
                                completion(slot.getURL, nil)
286
                            }
287
                        case .failure(let error):
288
                            completion(nil, error)
289
                            DDLogError("Upload error: \(error)")
290
                        }
291
                    }.uploadProgress(queue: self.internalQueue) { progress in
292
                        DDLogVerbose("Download progress \(progress.fractionCompleted)")
293
                        self.connection.asyncReadWrite { transaction in
294
                            if let media = media.refetch(with: transaction) {
295
                                media.transferProgress = Float(progress.fractionCompleted)
296
                                media.save(with: transaction)
297
                                media.touchParentMessage(with: transaction)
298
                            }
299
                        }
300
                }
301
            })
302
        }
303
    }
304
    
305
    @objc public func send(videoURL url: URL, thread: OTRThreadOwner) {
306
        internalQueue.async {
307
            self.send(url: url, thread: thread, type: .video)
308
        }
309
    }
310
    
311
    private enum MediaURLType {
312
        case audio
313
        case video
314
        //case image
315
    }
316
    
317
    private func send(url: URL, thread: OTRThreadOwner, type: MediaURLType) {
318
        internalQueue.async {
319
            var item: OTRMediaItem? = nil
320
            switch type {
321
            case .audio:
322
                item = OTRAudioItem(audioURL: url, isIncoming: false)
323
            case .video:
324
                item = OTRVideoItem(videoURL: url, isIncoming: false)
325
            }
326
            guard let mediaItem = item else {
327
                DDLogError("No media item to share for URL: \(url)")
328
                return
329
            }
330
            
331
            guard let message = self.newOutgoingMessage(to: thread, mediaItem: mediaItem) else {
332
                DDLogError("No message could be created for \(thread) \(mediaItem)")
333
                return
334
            }
335
            mediaItem.parentObjectKey = message.messageKey
336
            mediaItem.parentObjectCollection = message.messageCollection
337
            let newPath = OTRMediaFileManager.path(for: mediaItem, buddyUniqueId: thread.threadIdentifier)
338
            self.connection.readWrite { transaction in
339
                message.save(with: transaction)
340
                mediaItem.save(with: transaction)
341
                
342
            }
343
            OTRMediaFileManager.shared.copyData(fromFilePath: url.path, toEncryptedPath: newPath, completion: { (result, copyError: Error?) in
344
                var prefetchedData: Data? = nil
345
                if FileManager.default.fileExists(atPath: url.path) {
346
                    do {
347
                        let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
348
                        if let size = attributes[FileAttributeKey.size] as? NSNumber, size.uint64Value < 1024 * 1024 * 1 {
349
                            prefetchedData = try Data(contentsOf: url)
350
                        }
351
                    } catch let error {
352
                        DDLogError("Error prefetching data: \(error)")
353
                    }
354
                    do {
355
                        try FileManager.default.removeItem(atPath: url.path)
356
                    } catch let error {
357
                        DDLogError("Error removing video: \(error)")
358
                    }
359
                }
360
                message.messageError = copyError
361
                self.connection.readWrite({ (transaction) in
362
                    mediaItem.save(with: transaction)
363
                    message.save(with: transaction)
364
                })
365
                self.send(mediaItem: mediaItem, prefetchedData: prefetchedData, message: message)
366
            }, completionQueue: self.internalQueue)
367
        }
368
    }
369
    
370
    @objc public func send(audioURL url: URL, thread: OTRThreadOwner) {
371
        internalQueue.async {
372
            self.send(url: url, thread: thread, type: .audio)
373
        }
374
    }
375
    
376
    @objc public func send(image: UIImage, thread: OTRThreadOwner) {
377
        internalQueue.async {
378
            guard let service = self.servers.first, service.maxSize > 0 else {
379
                DDLogError("No HTTP upload service available!")
380
                return
381
            }
382
            let filename = "\(UUID().uuidString).jpg"
383
            let imageItem = OTRImageItem(filename: filename, size: image.size, mimeType: "image/jpeg", isIncoming: false)
384
            guard let message = self.newOutgoingMessage(to: thread, mediaItem: imageItem) else {
385
                DDLogError("No message could be created")
386
                return
387
            }
388
            imageItem.parentObjectKey = message.messageKey
389
            imageItem.parentObjectCollection = message.messageCollection
390
            self.connection.readWrite { transaction in
391
                message.save(with: transaction)
392
                imageItem.save(with: transaction)
393
            }
394
            guard let ourImageData = image.jpegData(dataSize: .unlimited, resize: UIImage.Quality.high, jpeg: UIImage.Quality.high, maxTries: 10) else {
395
                DDLogError("Could not make JPEG out of image!")
396
                return
397
            }
398
            OTRMediaFileManager.shared.setData(ourImageData, for: imageItem, buddyUniqueId: thread.threadIdentifier, completion: { (bytesWritten: Int, error: Error?) in
399
                self.connection.readWrite({ (transaction) in
400
                    imageItem.touchParentMessage(with: transaction)
401
                    if let error = error {
402
                        message.messageError = error
403
                        message.save(with: transaction)
404
                    }
405
                })
406
                if let imageData = image.jpegData(dataSize: .maxBytes(service.maxSize), resize: UIImage.Quality.medium, jpeg: UIImage.Quality.medium, maxTries: 10) {
407
                    self.send(mediaItem: imageItem, prefetchedData: imageData, message: message)
408
                } else {
409
                    DDLogError("Could not make JPEG out of image! Bad size")
410
                    message.messageError = FileTransferError.exceedsMaxSize
411
                    self.connection.readWrite { transaction in
412
                        message.save(with: transaction)
413
                    }
414
                }
415
            }, completionQueue: self.internalQueue)
416
        }
417
    }
418
    
419
    private func newOutgoingMessage(to thread: OTRThreadOwner, mediaItem: OTRMediaItem) -> OTRMessageProtocol? {
420
        if let buddy = thread as? OTRBuddy {
421
            let message = OTROutgoingMessage()!
422
            var security: OTRMessageTransportSecurity = .invalid
423
            self.connection.read({ (transaction) in
424
                security = buddy.preferredTransportSecurity(with: transaction)
425
            })
426
            message.buddyUniqueId = buddy.uniqueId
427
            message.mediaItemUniqueId = mediaItem.uniqueId
428
            message.messageSecurityInfo = OTRMessageEncryptionInfo(messageSecurity: security)
429
            return message
430
        } else if let room = thread as? OTRXMPPRoom {
431
            let message = OTRXMPPRoomMessage()!
432
            message.messageDate = Date()
433
            message.roomUniqueId = room.threadIdentifier
434
            message.roomJID = room.jid
435
            message.senderJID = room.ownJID
436
            message.state = .needsSending
437
            message.mediaItemId = mediaItem.uniqueId
438
            return message
439
        }
440
        return nil
441
    }
442
    
443
    @objc public func send(mediaItem: OTRMediaItem, prefetchedData: Data?, message: OTRMessageProtocol) {
444
        var shouldEncrypt = false
445
        switch message.messageSecurity {
446
        case .OMEMO, .OTR:
447
            shouldEncrypt = true
448
        case .invalid, .plaintext, .plaintextWithOTR:
449
            shouldEncrypt = false
450
        }
451
        
452
        self.upload(mediaItem: mediaItem, shouldEncrypt: shouldEncrypt, prefetchedData: prefetchedData, completion: { (_url: URL?, error: Error?) in
453
            guard let url = _url else {
454
                if let error = error {
455
                    DDLogError("Error uploading: \(error)")
456
                }
457
                self.connection.readWrite({ (transaction) in
458
                    message.messageError = error
459
                    message.save(with: transaction)
460
                })
461
                return
462
            }
463
            self.connection.readWrite({ (transaction) in
464
                mediaItem.transferProgress = 1.0
465
                message.messageText = url.absoluteString
466
                mediaItem.save(with: transaction)
467
                message.save(with: transaction)
468
            })
469
            self.queueOutgoingMessage(message: message)
470
        })
471
    }
472
    
473
    private func queueOutgoingMessage(message: OTRMessageProtocol) {
474
        let sendAction = OTRYapMessageSendAction(forMessage: message, date: nil)
475
        self.connection.readWrite { transaction in
476
            message.save(with: transaction)
477
            sendAction.save(with: transaction)
478
            if let thread = message.threadOwner(with: transaction)  {
479
                if let buddy = thread as? OTRBuddy {
480
                    buddy.lastMessageId = message.uniqueId
481
                    buddy.save(with: transaction)
482
                }
483
            }
484
        }
485
    }
486
    
487
    // MARK: - Private Methods
488
    
489
    private func serversFromCapabilities(capabilities: [XMPPJID : XMLElement]) -> [HTTPServer] {
490
        var servers: [HTTPServer] = []
491
        for (jid, element) in capabilities {
492
            let supported = element.supportsHTTPUpload()
493
            let maxSize = element.maxHTTPUploadSize()
494
            if supported && maxSize > 0 {
495
                let server = HTTPServer(jid: jid, maxSize: maxSize)
496
                servers.append(server)
497
            }
498
        }
499
        return servers
500
    }
501

    
502
    // MARK: - OTRServerCapabilitiesDelegate
503
    
504
    public func serverCapabilities(_ sender: OTRServerCapabilities, didDiscoverCapabilities capabilities: [XMPPJID : XMLElement]) {
505
        servers = serversFromCapabilities(capabilities: capabilities)
506
    }
507
}
508

    
509
// MARK: - Scanning and downloading incoming media
510
extension FileTransferManager {
511
    
512
    /** creates downloadmessages and then downloads if needed. parent message should already be saved! @warn Do not call from within an existing db transaction! */
513
    @objc public func createAndDownloadItemsIfNeeded(message: OTRMessageProtocol, readConnection: YapDatabaseConnection, force: Bool) {
514
        DispatchQueue.global(qos: .default).async {
515
            if message.messageMediaItemKey != nil || message.messageText?.count == 0 || message.downloadableURLs.count == 0 {
516
                //DDLogVerbose("Download of message not needed \(message.messageKey)")
517
                return
518
            }
519
            var downloads: [OTRDownloadMessage] = []
520
            var disableAutomaticURLFetching = false
521
            if !force {
522
                readConnection.read { (transaction) in
523
                    downloads = message.existingDownloads(with: transaction)
524
                    if let thread = message.threadOwner(with: transaction), let account = OTRAccount.fetchObject(withUniqueID: thread.threadAccountIdentifier, transaction: transaction) {
525
                        disableAutomaticURLFetching = account.disableAutomaticURLFetching
526
                        if !disableAutomaticURLFetching, let room = thread as? OTRXMPPRoom, let message = message as? OTRXMPPRoomMessage {
527
                            // For room messages, default to safe mode
528
                            disableAutomaticURLFetching = true
529
                            if let senderJidString = message.senderJID, let senderJID = XMPPJID(string: senderJidString), let roomJID = room.roomJID, let occupant = OTRXMPPRoomOccupant.occupant(jid: senderJID, realJID: senderJID, roomJID: roomJID, accountId: thread.threadAccountIdentifier, createIfNeeded: false, transaction: transaction), occupant.buddy(with: transaction) != nil {
530
                                // We have a buddy, i.e. we are friends with the sender.
531
                                disableAutomaticURLFetching = false
532
                            }
533
                        }
534
                    }
535
                }
536
            }
537
            if downloads.count == 0 {
538
                downloads = message.downloads()
539
                if downloads.count == 0 {
540
                    return
541
                }
542
                self.connection.readWrite({ (transaction) in
543
                    for download in downloads {
544
                        if disableAutomaticURLFetching,
545
                            let filename = download.downloadableURL?.absoluteString {
546
                            let media = OTRMediaItem.incomingItem(withFilename: filename, mimeType: nil)
547
                            media.parentObjectKey = download.uniqueId
548
                            media.parentObjectCollection = download.messageCollection
549
                            media.save(with: transaction)
550
                            download.messageMediaItemKey = media.uniqueId
551
                            download.messageError = FileTransferError.automaticDownloadsDisabled
552
                        }
553
                        download.save(with: transaction)
554
                    }
555
                    message.touch(with: transaction)
556
                })
557
            }
558
            if disableAutomaticURLFetching {
559
                DDLogVerbose("Automatic URL fetching disabled \(message.messageKey)")
560
                return
561
            }
562
            for download in downloads {
563
                self.downloadMediaIfNeeded(download)
564
            }
565
        }
566
    }
567
    
568
    /** Downloads media for a single downloadmessage */
569
    public func downloadMediaIfNeeded(_ downloadMessage: OTRDownloadMessage) {
570
        // Bail out if we've already downloaded the media
571
        if downloadMessage.messageMediaItemKey != nil &&
572
            downloadMessage.messageError == nil {
573
            // DDLogWarn("Already downloaded media for this item")
574
            return
575
        }
576
        guard let url = downloadMessage.downloadableURL else {
577
            DDLogWarn("Attempted to download message but couldn't parse a URL \(downloadMessage)")
578
            return
579
        }
580
        self.sessionManager.session.getTasksWithCompletionHandler { (tasks, _, _) in
581
            // Bail out if we've already got a task for this
582
            for task in tasks where task.originalRequest?.url == url {
583
                DDLogWarn("Already have outstanding task: \(task)")
584
                return
585
            }
586
            self.sessionManager.request(url, method: .head)
587
                .validate()
588
                .responseData(queue: self.internalQueue) { response in
589
                switch response.result {
590
                case .success:
591
                    DDLogInfo("HEAD response: \(String(describing: response.response?.allHeaderFields))")
592
                    if let headers = response.response?.allHeaderFields {
593
                        let contentType = headers["Content-Type"] as? String
594
                        let contentLength = headers["Content-Length"] as? UInt ?? 0
595
                        self.continueDownloading(downloadMessage: downloadMessage, url: url, contentType: contentType, contentLength: contentLength)
596
                    }
597
                case .failure(let error):
598
                    self.setError(error, onMessage: downloadMessage)
599
                    DDLogError("HEAD error: \(error)")
600
                }
601
            }
602
        }
603
    }
604
    
605
    private func setError(_ error: Error, onMessage downloadMessage: OTRDownloadMessage) {
606
        self.connection.readWrite { transaction in
607
            if let message = downloadMessage.refetch(with: transaction) {
608
                message.messageError = error
609
                message.save(with: transaction)
610
            }
611
        }
612
    }
613
    
614
    private func continueDownloading(downloadMessage: OTRDownloadMessage, url: URL, contentType: String?, contentLength: UInt) {
615
        var mediaItem: OTRMediaItem? = nil
616
        self.connection.readWrite { transaction in
617
            // Remove placeholder media item
618
            mediaItem = OTRMediaItem(forMessage: downloadMessage, transaction: transaction)
619
            mediaItem?.remove(with: transaction)
620
            mediaItem = OTRMediaItem.incomingItem(withFilename: url.lastPathComponent, mimeType: contentType)
621
            mediaItem?.parentObjectKey = downloadMessage.uniqueId
622
            mediaItem?.parentObjectCollection = downloadMessage.messageCollection
623
            mediaItem?.save(with: transaction)
624
            downloadMessage.messageMediaItemKey = mediaItem?.uniqueId
625
            downloadMessage.save(with: transaction)
626
        }
627
        guard let media = mediaItem else {
628
            DDLogError("Could not unwrap media item")
629
            self.setError(FileTransferError.fileNotFound, onMessage: downloadMessage)
630
            return
631
        }
632
        DDLogVerbose("Downloading media item at URL: \(url)")
633
        self.sessionManager.request(url)
634
            .validate()
635
            .responseData(queue: self.internalQueue) { response in
636
                self.finishDownload(downloadMessage: downloadMessage, mediaItem: media, inData: response.data, urlResponse: response.response, error: response.error)
637
                switch response.result {
638
                case .success:
639
                    DDLogVerbose("Download Successful")
640
                case .failure(let error):
641
                    self.setError(error, onMessage: downloadMessage)
642
                    DDLogError("Download Error \(error)")
643
                }
644
            }.downloadProgress(queue: self.internalQueue) { progress in
645
                DDLogVerbose("Download progress \(progress.fractionCompleted)")
646
                self.connection.asyncReadWrite { transaction in
647
                    if let media = media.refetch(with: transaction) {
648
                        media.transferProgress = Float(progress.fractionCompleted)
649
                        media.save(with: transaction)
650
                        media.touchParentMessage(with: transaction)
651
                    }
652
                }
653
        }
654
    }
655
    
656
    private func finishDownload(downloadMessage: OTRDownloadMessage, mediaItem: OTRMediaItem, inData: Data?, urlResponse: URLResponse?, error: Error?) {
657
        if let error = error {
658
            self.setError(error, onMessage: downloadMessage)
659
            DDLogError("Error downloading file \(error)")
660
            return
661
        }
662
        guard var data = inData, let response = urlResponse, let url = response.url else {
663
            self.setError(FileTransferError.fileNotFound, onMessage: downloadMessage)
664
            DDLogError("No data or response")
665
            return
666
        }
667
        DDLogVerbose("Received response \(response)")
668
        let authTagSize = 16 // i'm not sure if this can be assumed, but how else would we know the size?
669
        if let (key, iv) = url.aesGcmKey, data.count > authTagSize {
670
            DDLogVerbose("Received encrypted response, attempting decryption...")
671
            
672
            let cryptedData = data.subdata(in: 0..<data.count - authTagSize)
673
            let authTag = data.subdata(in: data.count - authTagSize..<data.count)
674
            let cryptoData = OTRCryptoData(data: cryptedData, authTag: authTag)
675
            do {
676
                data = try OTRCryptoUtility.decryptAESGCMData(cryptoData, key: key, iv: iv)
677
            } catch let error {
678
                self.setError(error, onMessage: downloadMessage)
679
                DDLogError("Error decrypting data: \(error)")
680
                return
681
            }
682
            DDLogVerbose("Decrpytion successful")
683
        }
684
        OTRMediaFileManager.sharedInstance().setData(data, for: mediaItem, buddyUniqueId: downloadMessage.threadId, completion: { (bytesWritten, error) in
685
            if let error = error {
686
                self.setError(error, onMessage: downloadMessage)
687
                DDLogError("Error copying data: \(error)")
688
                return
689
            }
690
            self.connection.asyncReadWrite({ (transaction) in
691
                mediaItem.transferProgress = 1.0
692
                
693
                if let audioItem = mediaItem as? OTRAudioItem, let url = OTRMediaServer.sharedInstance().url(for: mediaItem, buddyUniqueId: downloadMessage.threadId) {
694
                    audioItem.populateFromData(at: url)
695
                }
696
                
697
                mediaItem.save(with: transaction)
698
                // downloadMessage.save(with: transaction)
699
                if let message = downloadMessage.refetch(with: transaction) {
700
                    message.messageError = nil
701
                    message.save(with: transaction)
702
                } else {
703
                    DDLogError("Failed to refetch download message WTF \(downloadMessage)")
704
                }
705
            }, completionQueue: DispatchQueue.main,
706
               completionBlock: {
707
                UIApplication.shared.showLocalNotification(downloadMessage)
708
            })
709
        }, completionQueue: nil)
710
    }
711
}
712

    
713
extension OTRDownloadMessage {
714
    /// Turn aesgcm links into https links
715
    var downloadableURL: URL? {
716
        guard var downloadableURL = url else { return nil }
717
        if downloadableURL.isAesGcm, var components = URLComponents(url: downloadableURL, resolvingAgainstBaseURL: true) {
718
            components.scheme = URLScheme.https.rawValue
719
            if let rawURL = components.url {
720
                downloadableURL = rawURL
721
            }
722
        }
723
        return downloadableURL
724
    }
725
}
726

    
727
public extension OTRMessageProtocol {
728
    public var downloadableURLs: [URL] {
729
        return self.messageText?.downloadableURLs ?? []
730
    }
731
}
732

    
733
public extension OTRBaseMessage {
734
    @objc public var downloadableNSURLs: [NSURL] {
735
        return self.downloadableURLs as [NSURL]
736
    }
737
}
738

    
739
public extension OTRXMPPRoomMessage {
740
    @objc public var downloadableNSURLs: [NSURL] {
741
        return self.downloadableURLs as [NSURL]
742
    }
743
}
744

    
745
// MARK: - Extensions
746

    
747
fileprivate struct HTTPServer {
748
    /// service jid for upload service
749
    let jid: XMPPJID
750
    /// max upload size in bytes
751
    let maxSize: UInt
752
}
753

    
754
public extension XMLElement {
755
    
756
    // For use on a <query> element
757
    func supportsHTTPUpload() -> Bool {
758
        let features = self.elements(forName: "feature")
759
        var supported = false
760
        for feature in features {
761
            if let value = feature.attributeStringValue(forName: "var"),
762
                value == XMPPHTTPFileUploadNamespace  {
763
                supported = true
764
                break
765
            }
766
        }
767
        return supported
768
    }
769
    
770
    /// Returns 0 on failure, or max file size in bytes
771
    func maxHTTPUploadSize() -> UInt {
772
        var maxSize: UInt = 0
773
        let xes = self.elements(forXmlns: "jabber:x:data")
774
        
775
        for x in xes {
776
            let fields = x.elements(forName: "field")
777
            var correctXEP = false
778
            for field in fields {
779
                if let value = field.forName("value") {
780
                    if value.stringValue == XMPPHTTPFileUploadNamespace {
781
                        correctXEP = true
782
                    }
783
                    if let varMaxFileSize = field.attributeStringValue(forName: "var"), varMaxFileSize == "max-file-size" {
784
                        maxSize = value.stringValueAsNSUInteger()
785
                    }
786
                }
787
            }
788
            if correctXEP && maxSize > 0 {
789
                break
790
            }
791
        }
792
        
793
        return maxSize
794
    }
795
}
796

    
797
enum URLScheme: String {
798
    case https = "https"
799
    case aesgcm = "aesgcm"
800
    static let downloadableSchemes: [URLScheme] = [.https, .aesgcm]
801
}
802

    
803
extension URL {
804
    
805
    /** URL scheme matches aesgcm:// */
806
    var isAesGcm: Bool {
807
        return scheme == URLScheme.aesgcm.rawValue
808
    }
809
    
810
    /** Has hex anchor with key and IV. 48 bytes w/ 16 iv + 32 key */
811
    var anchorData: Data? {
812
        guard let anchor = self.fragment else { return nil }
813
        let data = anchor.dataFromHex()
814
        return data
815
    }
816
    
817
    var aesGcmKey: (key: Data, iv: Data)? {
818
        guard let data = self.anchorData, data.count == 48 else { return nil }
819
        let iv = data.subdata(in: 0..<16)
820
        let key = data.subdata(in: 16..<48)
821
        return (key, iv)
822
    }
823
}
824

    
825
public extension NSString {
826
    public var isSingleURLOnly: Bool {
827
        return (self as String).isSingleURLOnly
828
    }
829
}
830

    
831
public extension String {
832
    
833
    private var urlRanges: ([URL], [NSRange]) {
834
        guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
835
            return ([], [])
836
        }
837
        var urls: [URL] = []
838
        var ranges: [NSRange] = []
839
        let matches = detector.matches(in: self, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, self.count))
840
        for match in matches where match.resultType == .link {
841
            if let url = match.url {
842
                urls.append(url)
843
                ranges.append(match.range)
844
            }
845
        }
846
        return (urls, ranges)
847
    }
848
    
849
    /** Grab any URLs from a string */
850
    public var urls: [URL] {
851
        let (urls, _) = urlRanges
852
        return urls
853
    }
854
    
855
    /** Returns true if the message is ONLY a single URL */
856
    public var isSingleURLOnly: Bool {
857
        let (_, ranges) = urlRanges
858
        guard ranges.count == 1,
859
            let range = ranges.first,
860
            range.length == self.count else {
861
            return false
862
        }
863
        return true
864
    }
865
    
866
    /** Use this for extracting potentially downloadable URLs from a message. Currently checks for https:// and aesgcm:// */
867
    public var downloadableURLs: [URL] {
868
        return urlsMatchingSchemes(URLScheme.downloadableSchemes)
869
    }
870
    
871
    fileprivate func urlsMatchingSchemes(_ schemes: [URLScheme]) -> [URL] {
872
        let urls = self.urls.filter {
873
            guard let scheme = $0.scheme else { return false }
874
            for inScheme in schemes where inScheme.rawValue == scheme {
875
                return true
876
            }
877
            return false
878
        }
879
        return urls
880
    }
881
}
882

    
883
public extension FileTransferManager {
884
    /// Returns whether or not message should be displayed or hidden from collection. Single incoming URLs should be hidden, for example.
885
    @objc public static func shouldDisplayMessage(_ message: OTRMessageProtocol, transaction: YapDatabaseReadTransaction) -> Bool {
886
        // Always show media messages
887
        if message.messageMediaItemKey != nil {
888
            return true
889
        }
890
        // Always show downloads
891
        if message is OTRDownloadMessage {
892
            return true
893
        }
894
        // Hide non-media messages that have no text
895
        guard let messageText = message.messageText else {
896
            return false
897
        }
898
        
899
        // Filter out messages that are aesgcm scheme file transfers
900
        if messageText.contains("aesgcm://"),
901
            message.messageError == nil {
902
            return false
903
        }
904
        
905
        // Filter out messages that are just URLs and have downloads
906
        if messageText.isSingleURLOnly,
907
            message.hasExistingDownloads(with: transaction) {
908
            return false
909
        }
910

    
911
        return true
912
    }
913
}