mirror of
https://github.com/Codeux-Software/Textual.git
synced 2026-06-16 13:24:32 +00:00
1275 lines
33 KiB
Objective-C
Executable File
1275 lines
33 KiB
Objective-C
Executable File
/* *********************************************************************
|
|
* _____ _ _
|
|
* |_ _|____ _| |_ _ _ __ _| |
|
|
* | |/ _ \ \/ / __| | | |/ _` | |
|
|
* | | __/> <| |_| |_| | (_| | |
|
|
* |_|\___/_/\_\\__|\__,_|\__,_|_|
|
|
*
|
|
* Copyright (c) 2008 - 2010 Satoshi Nakagawa <psychs AT limechat DOT net>
|
|
* Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors.
|
|
* Please see Acknowledgements.pdf for additional information.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* * Neither the name of Textual, "Codeux Software, LLC", nor the
|
|
* names of its contributors may be used to endorse or promote products
|
|
* derived from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*
|
|
*********************************************************************** */
|
|
|
|
#import "NSObjectHelperPrivate.h"
|
|
#import "GCDAsyncSocket.h"
|
|
#import "TXGlobalModels.h"
|
|
#import "IRCClientPrivate.h"
|
|
#import "TPCPathInfo.h"
|
|
#import "TPCPreferencesLocal.h"
|
|
#import "TLOEncryptionManagerPrivate.h"
|
|
#import "TLOLocalization.h"
|
|
#import "TDCFileTransferDialogTableCellPrivate.h"
|
|
#import "TDCFileTransferDialogTransferControllerPrivate.h"
|
|
#import "TDCFileTransferDialogInternal.h"
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
#define RECORDS_LENGTH 10
|
|
#define MAX_QUEUE_SIZE 2
|
|
#define BUFFER_SIZE (1024 * 64)
|
|
#define RATE_LIMIT (1024 * 1024 * 10)
|
|
|
|
#define _connectTimeout 30.0
|
|
#define _sendDataTimeout 30.0
|
|
#define _resumeAcceptTimeout 10.0
|
|
|
|
@interface TDCFileTransferDialogTransferController ()
|
|
@property (nonatomic, strong, readwrite) IRCClient *client;
|
|
@property (nonatomic, copy, readwrite) NSString *clientId;
|
|
@property (nonatomic, assign, readwrite) BOOL isResume;
|
|
@property (nonatomic, assign, readwrite) BOOL isReversed;
|
|
@property (nonatomic, assign, readwrite) BOOL isSender;
|
|
@property (nonatomic, assign, readwrite) TDCFileTransferDialogTransferStatus transferStatus;
|
|
@property (nonatomic, assign, readwrite) uint64_t totalFilesize;
|
|
@property (nonatomic, assign, readwrite) uint64_t processedFilesize;
|
|
@property (nonatomic, assign, readwrite) uint64_t currentRecord;
|
|
@property (nonatomic, strong) NSMutableArray<NSNumber *> *speedRecordsPrivate;
|
|
@property (nonatomic, copy, readwrite, nullable) NSString *errorMessageDescription;
|
|
@property (nonatomic, copy, readwrite, nullable) NSString *path;
|
|
@property (nonatomic, copy, readwrite) NSString *filename;
|
|
@property (nonatomic, copy, readwrite) NSString *hostAddress;
|
|
@property (nonatomic, copy, readwrite) NSString *peerNickname;
|
|
@property (nonatomic, copy, readwrite, nullable) NSString *transferToken;
|
|
@property (nonatomic, copy, readwrite) NSString *uniqueIdentifier;
|
|
@property (nonatomic, assign, readwrite) uint16_t hostPort;
|
|
@property (nonatomic, strong, nullable) NSFileHandle *fileHandle;
|
|
@property (nonatomic, strong, nullable) XRPortMapper *portMapping;
|
|
@property (nonatomic, assign) NSUInteger sendQueueSize;
|
|
@property (nonatomic, strong, nullable) dispatch_queue_t serverDispatchQueue;
|
|
@property (nonatomic, strong, nullable) dispatch_queue_t serverSocketQueue;
|
|
@property (nonatomic, strong, nullable) GCDAsyncSocket *listeningServer;
|
|
@property (nonatomic, strong, nullable) GCDAsyncSocket *listeningServerConnectedClient;
|
|
@property (nonatomic, strong, nullable) GCDAsyncSocket *connectionToRemoteServer;
|
|
@property (nonatomic, strong, nullable) id transferProgressHandler; // Used to prevent system sleep
|
|
@property (readonly) uint64_t currentFilesize;
|
|
@property (readonly) TDCFileTransferDialog *transferDialog;
|
|
@property (readonly, nullable) GCDAsyncSocket *readSocket;
|
|
@property (readonly, nullable) GCDAsyncSocket *writeSocket;
|
|
@end
|
|
|
|
@implementation TDCFileTransferDialogTransferController
|
|
|
|
#pragma mark -
|
|
#pragma mark Initialization
|
|
|
|
+ (nullable instancetype)receiverForClient:(IRCClient *)client nickname:(NSString *)nickname address:(NSString *)hostAddress port:(uint16_t)hostPort filename:(NSString *)filename filesize:(uint64_t)totalFilesize token:(nullable NSString *)transferToken
|
|
{
|
|
NSParameterAssert(client != nil);
|
|
NSParameterAssert(nickname != nil);
|
|
NSParameterAssert(hostAddress != nil);
|
|
NSParameterAssert(filename != nil);
|
|
|
|
/* Construct controller */
|
|
TDCFileTransferDialogTransferController *controller = [[self alloc] initWithClient:client];
|
|
|
|
if (transferToken.length > 0) {
|
|
controller.transferToken = transferToken;
|
|
|
|
controller.isReversed = YES;
|
|
}
|
|
|
|
controller.isSender = NO;
|
|
|
|
controller.peerNickname = nickname;
|
|
|
|
controller.hostAddress = hostAddress;
|
|
controller.hostPort = hostPort;
|
|
|
|
controller.filename = filename;
|
|
|
|
controller.totalFilesize = totalFilesize;
|
|
|
|
return controller;
|
|
}
|
|
|
|
+ (nullable instancetype)senderForClient:(IRCClient *)client nickname:(NSString *)nickname path:(NSString *)path
|
|
{
|
|
NSParameterAssert(client != nil);
|
|
NSParameterAssert(nickname != nil);
|
|
NSParameterAssert(path != nil);
|
|
|
|
NSString *filename = path.lastPathComponent;
|
|
|
|
#if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1
|
|
if ([TPCPreferences textEncryptionIsEnabled]) {
|
|
/* Ask whether we should be allowed to add the file. */
|
|
BOOL allowWithOTR = [sharedEncryptionManager()
|
|
safeToTransferFile:filename
|
|
to:[client encryptionAccountNameForUser:nickname]
|
|
from:[client encryptionAccountNameForLocalUser]
|
|
isIncomingFileTransfer:NO];
|
|
|
|
if (allowWithOTR == NO) {
|
|
return nil; // This operation is not allowed...
|
|
}
|
|
}
|
|
#endif
|
|
|
|
/* Gather file information */
|
|
NSDictionary *fileAttributes = [RZFileManager() attributesOfItemAtPath:path error:NULL];
|
|
|
|
if (fileAttributes == nil) {
|
|
return nil;
|
|
}
|
|
|
|
uint64_t totalFilesize = [fileAttributes fileSize];
|
|
|
|
if (totalFilesize == 0) {
|
|
LogToConsoleError("Fatal error: Cannot create sender because filesize == 0");
|
|
|
|
return nil;
|
|
}
|
|
|
|
NSString *filePath = path.stringByDeletingLastPathComponent;
|
|
|
|
/* Construct controller */
|
|
TDCFileTransferDialogTransferController *controller = [[self alloc] initWithClient:client];
|
|
|
|
controller.isReversed = [TPCPreferences fileTransferRequestsAreReversed];
|
|
|
|
controller.isSender = YES;
|
|
|
|
controller.peerNickname = nickname;
|
|
|
|
controller.path = filePath;
|
|
controller.filename = filename;
|
|
|
|
controller.totalFilesize = totalFilesize;
|
|
|
|
return controller;
|
|
}
|
|
|
|
- (instancetype)init
|
|
{
|
|
[self doesNotRecognizeSelector:_cmd];
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (instancetype)initWithClient:(IRCClient *)client
|
|
{
|
|
NSParameterAssert(client != nil);
|
|
|
|
if ((self = [super init])) {
|
|
self.client = client;
|
|
self.clientId = client.uniqueIdentifier;
|
|
|
|
[self prepareInitialState];
|
|
|
|
return self;
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (void)prepareInitialState
|
|
{
|
|
self.speedRecordsPrivate = [NSMutableArray array];
|
|
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusStopped;
|
|
|
|
self.uniqueIdentifier = [NSString stringWithUUID];
|
|
|
|
[RZNotificationCenter() addObserver:self selector:@selector(clientDisconnected:) name:IRCClientDidDisconnectNotification object:self.client];
|
|
[RZNotificationCenter() addObserver:self selector:@selector(peerNicknameChanged:) name:IRCClientUserNicknameChangedNotification object:self.client];
|
|
}
|
|
|
|
- (void)prepareForPermanentDestruction
|
|
{
|
|
self.transferTableCell = nil;
|
|
|
|
[self closeAndPostNotification:NO];
|
|
|
|
[RZNotificationCenter() removeObserver:self];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Error Handling
|
|
|
|
- (void)failWithNoSpaceLeftOnDevice
|
|
{
|
|
[self closeWithLocalizedError:TXTLS(@"TDCFileTransferDialog[79f-s0]")];
|
|
}
|
|
|
|
- (void)closeWithLocalizedError:(NSString *)errorLocalization
|
|
{
|
|
[self closeWithLocalizedError:errorLocalization description:nil isFatalError:NO];
|
|
}
|
|
|
|
- (void)closeWithLocalizedError:(NSString *)errorLocalization isFatalError:(BOOL)isFatalError
|
|
{
|
|
[self closeWithLocalizedError:errorLocalization description:nil isFatalError:isFatalError];
|
|
}
|
|
|
|
- (void)closeWithLocalizedError:(NSString *)errorLocalization description:(nullable NSString *)errorDescription
|
|
{
|
|
[self closeWithLocalizedError:errorLocalization description:errorDescription isFatalError:NO];
|
|
}
|
|
|
|
- (void)closeWithLocalizedError:(NSString *)errorLocalization description:(nullable NSString *)errorDescription isFatalError:(BOOL)isFatalError
|
|
{
|
|
NSParameterAssert(errorLocalization != nil);
|
|
|
|
if (errorDescription == nil) {
|
|
self.errorMessageDescription = TXTLS(errorLocalization, self.peerNickname);
|
|
} else {
|
|
self.errorMessageDescription = TXTLS(errorLocalization, self.peerNickname, errorDescription);
|
|
}
|
|
|
|
if (isFatalError) {
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusFatalError;
|
|
} else {
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusRecoverableError;
|
|
}
|
|
|
|
[self close];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Dispatch Queue Management
|
|
|
|
- (void)createDispatchQueues
|
|
{
|
|
NSString *uniqueId = [NSString stringWithUUID];
|
|
|
|
NSString *dispatchQueueName = [NSString stringWithFormat:@"Textual.TDCFileTransferDialogTransferController.DCC-SocketDispatchQueue-%@", uniqueId];
|
|
|
|
self.serverDispatchQueue =
|
|
XRCreateDispatchQueueWithPriority(dispatchQueueName.UTF8String, DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT);
|
|
|
|
NSString *socketQueueName = [NSString stringWithFormat:@"Textual.TDCFileTransferDialogTransferController.DCC-SocketReadWriteQueue-%@", uniqueId];
|
|
|
|
self.serverSocketQueue =
|
|
XRCreateDispatchQueueWithPriority(socketQueueName.UTF8String, DISPATCH_QUEUE_SERIAL, QOS_CLASS_DEFAULT);
|
|
}
|
|
|
|
- (void)destroyDispatchQueues
|
|
{
|
|
self.serverDispatchQueue = nil;
|
|
|
|
self.serverSocketQueue = nil;
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Opening/Closing Transfer
|
|
|
|
- (void)disableSystemSleep
|
|
{
|
|
self.transferProgressHandler = [RZProcessInfo() beginActivityWithOptions:NSActivityUserInitiated reason:@"Transferring file"];
|
|
}
|
|
|
|
- (void)enableSystemSleep
|
|
{
|
|
if (self.transferProgressHandler == nil) {
|
|
return;
|
|
}
|
|
|
|
[RZProcessInfo() endActivity:self.transferProgressHandler];
|
|
|
|
self.transferProgressHandler = nil;
|
|
}
|
|
|
|
- (BOOL)receiveUnencryptedFile
|
|
{
|
|
#if TEXTUAL_BUILT_WITH_ADVANCED_ENCRYPTION == 1
|
|
if ([TPCPreferences textEncryptionIsEnabled]) {
|
|
BOOL allowWithOTR = [sharedEncryptionManager()
|
|
safeToTransferFile:self.filename
|
|
to:[self.client encryptionAccountNameForUser:self.peerNickname]
|
|
from:[self.client encryptionAccountNameForLocalUser]
|
|
isIncomingFileTransfer:YES];
|
|
|
|
if (allowWithOTR == NO) {
|
|
return NO; // This operation is not allowed...
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)open
|
|
{
|
|
[self openWithPath:nil];
|
|
}
|
|
|
|
- (void)openWithPathOrUserDownloads
|
|
{
|
|
NSString *path = nil;
|
|
|
|
if (self.path == nil) {
|
|
path = [TPCPathInfo userDownloads];
|
|
}
|
|
|
|
[self openWithPath:path];
|
|
}
|
|
|
|
- (void)openWithPath:(nullable NSString *)path
|
|
{
|
|
if (self.path == nil) {
|
|
self.path = path;
|
|
}
|
|
|
|
if (self.client.isLoggedIn == NO) {
|
|
[self _closeWithClientDisconnectedError];
|
|
|
|
return;
|
|
}
|
|
|
|
if (self.isSender) {
|
|
[self openTransfer];
|
|
} else {
|
|
if ([self receiveUnencryptedFile] == NO) {
|
|
return;
|
|
}
|
|
|
|
[self sendTransferResumeRequestToClient];
|
|
}
|
|
}
|
|
|
|
- (void)openTransfer
|
|
{
|
|
if (self.isSender) {
|
|
if (self.isReversed) {
|
|
[self updateIPAddress];
|
|
} else {
|
|
[self openConnectionAsServer];
|
|
}
|
|
} else {
|
|
if (self.isReversed) {
|
|
[self openConnectionAsServer];
|
|
} else {
|
|
[self openConnectionToHost];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)openConnectionToHost
|
|
{
|
|
[self closeAndPostNotification:NO];
|
|
|
|
[self resetProperties];
|
|
|
|
[self createDispatchQueues];
|
|
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusConnecting;
|
|
|
|
GCDAsyncSocket *connectionToRemoteServer =
|
|
[[GCDAsyncSocket alloc] initWithDelegate:(id)self
|
|
delegateQueue:self.serverDispatchQueue
|
|
socketQueue:self.serverSocketQueue];
|
|
|
|
NSError *connectionError = nil;
|
|
|
|
BOOL isConnected = NO;
|
|
|
|
/* Use the interface of the configured IP address instead of the default. */
|
|
/* Default interface is used if IP address is not found locally. */
|
|
NSString *networkInterface = [self networkInterfaceMatchingAddress];
|
|
|
|
if (networkInterface) {
|
|
isConnected = [connectionToRemoteServer connectToHost:self.hostAddress onPort:self.hostPort viaInterface:networkInterface withTimeout:_connectTimeout error:&connectionError];
|
|
} else {
|
|
isConnected = [connectionToRemoteServer connectToHost:self.hostAddress onPort:self.hostPort withTimeout:_connectTimeout error:&connectionError];
|
|
}
|
|
|
|
if (isConnected == NO) {
|
|
if (connectionError) {
|
|
LogToConsoleError("DCC Connect Error: %{public}@", connectionError.localizedDescription);
|
|
}
|
|
|
|
[self closeWithLocalizedError:@"TDCFileTransferDialog[fn8-sx]"];
|
|
|
|
return;
|
|
}
|
|
|
|
self.connectionToRemoteServer = connectionToRemoteServer;
|
|
|
|
[self disableSystemSleep];
|
|
}
|
|
|
|
- (void)openConnectionAsServer
|
|
{
|
|
[self closeAndPostNotification:NO];
|
|
|
|
[self resetProperties];
|
|
|
|
[self createDispatchQueues];
|
|
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusInitializing;
|
|
|
|
self.hostPort = [TPCPreferences fileTransferPortRangeStart];
|
|
|
|
while ([self tryToOpenConnectionAsServer] == NO) {
|
|
self.hostPort += 1;
|
|
|
|
if (self.hostPort > [TPCPreferences fileTransferPortRangeEnd]) {
|
|
[self closeWithLocalizedError:@"TDCFileTransferDialog[vxc-sd]"];
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
[self disableSystemSleep];
|
|
}
|
|
|
|
- (BOOL)tryToOpenConnectionAsServer
|
|
{
|
|
GCDAsyncSocket *listeningServer =
|
|
[[GCDAsyncSocket alloc] initWithDelegate:(id)self
|
|
delegateQueue:self.serverDispatchQueue
|
|
socketQueue:self.serverSocketQueue];
|
|
|
|
BOOL isActive = [listeningServer acceptOnPort:self.hostPort error:NULL];
|
|
|
|
if (isActive == NO) {
|
|
return NO;
|
|
}
|
|
|
|
self.listeningServer = listeningServer;
|
|
|
|
/* Try to map the port */
|
|
XRPortMapper *portMapping = [[XRPortMapper alloc] initWithPort:self.hostPort];
|
|
|
|
portMapping.mapTCP = YES;
|
|
portMapping.mapUDP = NO;
|
|
|
|
portMapping.desiredPublicPort = self.hostPort;
|
|
|
|
self.portMapping = portMapping;
|
|
|
|
[RZNotificationCenter() addObserver:self selector:@selector(portMapperDidFinishWork:) name:XRPortMapperDidChangedNotification object:self.portMapping];
|
|
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusMappingListeningPort;
|
|
|
|
if ([self.portMapping open] == NO) {
|
|
[self portMapperDidFinishWork:nil];
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (nullable NSString *)networkInterfaceMatchingAddress
|
|
{
|
|
return [TPCPreferences fileTransferIPAddressInterfaceName];
|
|
}
|
|
|
|
- (void)portMapperDidFinishWork:(NSNotification *)aNotification
|
|
{
|
|
NSAssertReturn(self.transferStatus == TDCFileTransferDialogTransferStatusMappingListeningPort);
|
|
|
|
if (self.portMapping.isMapped) {
|
|
[self updateIPAddress];
|
|
|
|
LogToConsoleInfo("Successful port mapping on port %{public}hu", self.hostPort);
|
|
|
|
return;
|
|
}
|
|
|
|
LogToConsoleError("Port mapping failed with error code: %{public}i", self.portMapping.error);
|
|
|
|
if (self.isReversed) {
|
|
[self closeWithLocalizedError:@"TDCFileTransferDialog[vxc-sd]"];
|
|
|
|
return;
|
|
}
|
|
|
|
/* If mapping fails, we silently fail */
|
|
/* We tried and it was successful, then that is good, but if we
|
|
did not, still start listening just incase other conditions allow
|
|
the transfer to still take place. */
|
|
[self updateIPAddress];
|
|
}
|
|
|
|
- (void)updateIPAddress
|
|
{
|
|
LogStackTraceWithType(LogToConsoleTypeDebug);
|
|
|
|
NSString *address = self.transferDialog.IPAddress;
|
|
|
|
LogToConsoleDebug("TDCFileTransferDialog cached IP address: %{private}@", address);
|
|
|
|
TXFileTransferIPAddressMethodDetection detectionMethod = [TPCPreferences fileTransferIPAddressDetectionMethod];
|
|
|
|
BOOL manuallyDetect = (detectionMethod == TXFileTransferIPAddressMethodManual);
|
|
|
|
if (address == nil && manuallyDetect == NO) {
|
|
NSString *publicAddress = self.portMapping.publicAddress;
|
|
|
|
LogToConsoleDebug("Port mapper public IP address: %{private}@", publicAddress);
|
|
|
|
if (publicAddress.isIPAddress) {
|
|
self.transferDialog.IPAddress = publicAddress;
|
|
|
|
address = publicAddress;
|
|
}
|
|
}
|
|
|
|
/* Request address? */
|
|
if (address == nil) {
|
|
if (manuallyDetect || detectionMethod == TXFileTransferIPAddressMethodRouterOnly) {
|
|
LogToConsoleError("User has set IP address detection to be manual but have no address set");
|
|
|
|
[self noteIPAddressLookupFailed];
|
|
} else {
|
|
LogToConsoleDebug("Performing IP address lookup using the Internet");
|
|
|
|
[self.transferDialog requestIPAddress];
|
|
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusWaitingForLocalIPAddress;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
[self noteIPAddressLookupSucceeded];
|
|
}
|
|
|
|
- (void)closePortMapping
|
|
{
|
|
if (self.portMapping == nil) {
|
|
return;
|
|
}
|
|
|
|
[RZNotificationCenter() removeObserver:self name:XRPortMapperDidChangedNotification object:self.portMapping];
|
|
|
|
XRPerformBlockSynchronouslyOnMainQueue(^{
|
|
[self.portMapping close];
|
|
|
|
self.portMapping = nil;
|
|
});
|
|
}
|
|
|
|
- (void)noteIPAddressLookupSucceeded
|
|
{
|
|
LogStackTraceWithType(LogToConsoleTypeDebug);
|
|
|
|
if (self.isSender) {
|
|
if (self.isReversed) {
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusWaitingForReceiverToAccept;
|
|
} else {
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusIsListeningAsSender;
|
|
}
|
|
} else {
|
|
if (self.isReversed) {
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusIsListeningAsReceiver;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
[self sendTransferRequestToClient];
|
|
}
|
|
|
|
- (void)noteIPAddressLookupFailed
|
|
{
|
|
[self closeWithLocalizedError:@"TDCFileTransferDialog[47s-1s]"];
|
|
}
|
|
|
|
- (void)didReceiveResumeRequest:(uint64_t)proposedPosition
|
|
{
|
|
uint64_t currentFilesize = self.currentFilesize;
|
|
|
|
if (proposedPosition == 0 || currentFilesize < proposedPosition) {
|
|
return;
|
|
}
|
|
|
|
self.isResume = YES;
|
|
|
|
self.processedFilesize = proposedPosition;
|
|
|
|
[self sendTransferResumeAcceptToClient];
|
|
}
|
|
|
|
- (void)didReceiveResumeAccept:(uint64_t)proposedPosition
|
|
{
|
|
[self cancelPerformRequestsWithSelector:@selector(transferResumeRequestTimeout) object:nil];
|
|
|
|
uint64_t currentFilesize = self.currentFilesize;
|
|
|
|
if (currentFilesize != proposedPosition) {
|
|
[self closeWithLocalizedError:@"TDCFileTransferDialog[0ov-tr]" isFatalError:YES];
|
|
|
|
return;
|
|
}
|
|
|
|
self.isResume = YES;
|
|
|
|
self.processedFilesize = currentFilesize;
|
|
|
|
[self openTransfer];
|
|
}
|
|
|
|
- (void)didReceiveSendRequest:(NSString *)hostAddress hostPort:(uint16_t)hostPort
|
|
{
|
|
NSParameterAssert(hostAddress != nil);
|
|
|
|
self.hostAddress = hostAddress;
|
|
|
|
self.hostPort = hostPort;
|
|
|
|
self.processedFilesize = 0;
|
|
|
|
[self openConnectionToHost];
|
|
}
|
|
|
|
- (void)buildTransferToken
|
|
{
|
|
NSUInteger loopedCount = 0;
|
|
|
|
do {
|
|
NSString *transferToken = [NSString stringWithUnsignedInteger:TXRandomNumber(9999)];
|
|
|
|
BOOL transferExists = [self.transferDialog fileTransferExistsWithToken:transferToken];
|
|
|
|
if (transferExists == NO) {
|
|
self.transferToken = transferToken;
|
|
|
|
break;
|
|
}
|
|
|
|
loopedCount += 1;
|
|
} while (loopedCount < 300);
|
|
}
|
|
|
|
- (void)sendTransferRequestToClient
|
|
{
|
|
if (self.isSender) {
|
|
uint64_t currentFilesize = self.currentFilesize;
|
|
|
|
if (self.isReversed) {
|
|
[self buildTransferToken];
|
|
|
|
[self.client sendFile:self.peerNickname port:0 filename:self.filename filesize:currentFilesize token:self.transferToken];
|
|
} else {
|
|
[self.client sendFile:self.peerNickname port:self.hostPort filename:self.filename filesize:currentFilesize token:nil];
|
|
}
|
|
} else {
|
|
if (self.isReversed) {
|
|
[self.client sendFile:self.peerNickname port:self.hostPort filename:self.filename filesize:self.totalFilesize token:self.transferToken];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)sendTransferResumeRequestToClient
|
|
{
|
|
uint64_t currentFilesize = self.currentFilesize;
|
|
|
|
if (currentFilesize == 0 || currentFilesize > self.totalFilesize) {
|
|
[self transferResumeRequestTimeout];
|
|
|
|
return;
|
|
}
|
|
|
|
[self performSelectorInCommonModes:@selector(transferResumeRequestTimeout) withObject:nil afterDelay:_resumeAcceptTimeout];
|
|
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusWaitingForResumeAccept;
|
|
|
|
if (self.isReversed) {
|
|
[self.client sendFileResume:self.peerNickname port:0 filename:self.filename filesize:currentFilesize token:self.transferToken];
|
|
} else {
|
|
[self.client sendFileResume:self.peerNickname port:self.hostPort filename:self.filename filesize:currentFilesize token:nil];
|
|
}
|
|
}
|
|
|
|
- (void)sendTransferResumeAcceptToClient
|
|
{
|
|
if (self.isReversed) {
|
|
[self.client sendFileResumeAccept:self.peerNickname port:0 filename:self.filename filesize:self.processedFilesize token:self.transferToken];
|
|
} else {
|
|
[self.client sendFileResumeAccept:self.peerNickname port:self.hostPort filename:self.filename filesize:self.processedFilesize token:nil];
|
|
}
|
|
}
|
|
|
|
- (void)transferResumeRequestTimeout
|
|
{
|
|
[self openTransfer];
|
|
}
|
|
|
|
- (void)peerNicknameChanged:(NSNotification *)notification
|
|
{
|
|
NSDictionary *userInfo = notification.userInfo;
|
|
|
|
NSString *oldNickname = userInfo[@"oldNickname"];
|
|
|
|
if ([self.peerNickname isEqualToString:oldNickname] == NO) {
|
|
return;
|
|
}
|
|
|
|
self.peerNickname = userInfo[@"newNickname"];
|
|
}
|
|
|
|
- (void)clientDisconnected:(NSNotification *)notification
|
|
{
|
|
[self closeWithClientDisconnectedError];
|
|
}
|
|
|
|
- (void)closeWithClientDisconnectedError
|
|
{
|
|
/* If the controller is already sending or receiving data, then a connection
|
|
is already established to the peer which can function without a connection
|
|
to IRC. If data is not being transferred then fail immediately. */
|
|
TDCFileTransferDialogTransferStatus transferStatus = self.transferStatus;
|
|
|
|
if (transferStatus != TDCFileTransferDialogTransferStatusConnecting &&
|
|
transferStatus != TDCFileTransferDialogTransferStatusInitializing &&
|
|
transferStatus != TDCFileTransferDialogTransferStatusIsListeningAsReceiver &&
|
|
transferStatus != TDCFileTransferDialogTransferStatusIsListeningAsSender &&
|
|
transferStatus != TDCFileTransferDialogTransferStatusMappingListeningPort &&
|
|
transferStatus != TDCFileTransferDialogTransferStatusWaitingForLocalIPAddress &&
|
|
transferStatus != TDCFileTransferDialogTransferStatusWaitingForReceiverToAccept &&
|
|
transferStatus != TDCFileTransferDialogTransferStatusWaitingForResumeAccept)
|
|
{
|
|
return;
|
|
}
|
|
|
|
[self _closeWithClientDisconnectedError];
|
|
}
|
|
|
|
- (void)_closeWithClientDisconnectedError
|
|
{
|
|
[self closeWithLocalizedError:@"TDCFileTransferDialog[12p-0v]" isFatalError:NO];
|
|
}
|
|
|
|
- (void)close
|
|
{
|
|
[self closeAndPostNotification:YES];
|
|
}
|
|
|
|
- (void)closeAndPostNotification:(BOOL)postNotification
|
|
{
|
|
[self cancelPerformRequests];
|
|
|
|
if ( self.listeningServer) {
|
|
[self.listeningServer disconnect];
|
|
self.listeningServer = nil;
|
|
}
|
|
|
|
if ( self.listeningServerConnectedClient) {
|
|
[self.listeningServerConnectedClient disconnect];
|
|
self.listeningServerConnectedClient = nil;
|
|
}
|
|
|
|
if ( self.connectionToRemoteServer) {
|
|
[self.connectionToRemoteServer disconnect];
|
|
self.connectionToRemoteServer = nil;
|
|
}
|
|
|
|
[self destroyDispatchQueues];
|
|
|
|
[self closePortMapping];
|
|
|
|
[self closeFileHandle];
|
|
|
|
if (self.transferStatus != TDCFileTransferDialogTransferStatusComplete &&
|
|
self.transferStatus != TDCFileTransferDialogTransferStatusFatalError &&
|
|
self.transferStatus != TDCFileTransferDialogTransferStatusRecoverableError)
|
|
{
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusStopped;
|
|
}
|
|
|
|
if (postNotification) {
|
|
if (self.transferStatus == TDCFileTransferDialogTransferStatusFatalError ||
|
|
self.transferStatus == TDCFileTransferDialogTransferStatusRecoverableError)
|
|
{
|
|
if (self.isSender) {
|
|
[self.client notifyFileTransfer:TXNotificationTypeFileTransferSendFailed nickname:self.peerNickname filename:self.filename filesize:self.totalFilesize requestIdentifier:self.uniqueIdentifier];
|
|
} else {
|
|
[self.client notifyFileTransfer:TXNotificationTypeFileTransferReceiveFailed nickname:self.peerNickname filename:self.filename filesize:self.totalFilesize requestIdentifier:self.uniqueIdentifier];
|
|
}
|
|
}
|
|
else if (self.transferStatus == TDCFileTransferDialogTransferStatusComplete)
|
|
{
|
|
if (self.isSender) {
|
|
[self.client notifyFileTransfer:TXNotificationTypeFileTransferSendSuccessful nickname:self.peerNickname filename:self.filename filesize:self.totalFilesize requestIdentifier:self.uniqueIdentifier];
|
|
} else {
|
|
[self.client notifyFileTransfer:TXNotificationTypeFileTransferReceiveSuccessful nickname:self.peerNickname filename:self.filename filesize:self.totalFilesize requestIdentifier:self.uniqueIdentifier];
|
|
}
|
|
}
|
|
}
|
|
|
|
[self.transferDialog updateMaintenanceTimer];
|
|
|
|
[self enableSystemSleep];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Timer
|
|
|
|
- (void)onMaintenanceTimer
|
|
{
|
|
NSAssertReturn(self.transferStatus == TDCFileTransferDialogTransferStatusReceiving ||
|
|
self.transferStatus == TDCFileTransferDialogTransferStatusSending);
|
|
|
|
XRPerformBlockSynchronouslyOnQueue(self.serverDispatchQueue, ^{
|
|
@synchronized(self.speedRecords) {
|
|
[self.speedRecordsPrivate addObject:@(self.currentRecord)];
|
|
|
|
if (self.speedRecordsPrivate.count > RECORDS_LENGTH) {
|
|
[self.speedRecordsPrivate removeObjectAtIndex:0];
|
|
}
|
|
}
|
|
|
|
self.currentRecord = 0;
|
|
|
|
[self reloadStatusInformation];
|
|
});
|
|
|
|
[self send];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark File Handle
|
|
|
|
- (void)setNonexistentFilename
|
|
{
|
|
NSString *filePath = self.filePath;
|
|
|
|
if ([RZFileManager() fileExistsAtPath:filePath] == NO) {
|
|
return;
|
|
}
|
|
|
|
NSString *filenameExtension = self.filename.pathExtension;
|
|
|
|
NSString *filenameWithoutExtension = self.filename.stringByDeletingPathExtension;
|
|
|
|
NSUInteger i = 1;
|
|
|
|
do {
|
|
NSString *newFilename = nil;
|
|
|
|
if (filenameExtension.length == 0) {
|
|
newFilename = [NSString stringWithFormat:@"%@_%lu", filenameWithoutExtension, i];
|
|
} else {
|
|
newFilename = [NSString stringWithFormat:@"%@_%lu.%@", filenameWithoutExtension, i, filenameExtension];
|
|
}
|
|
|
|
filePath = [self.path stringByAppendingPathComponent:newFilename];
|
|
|
|
i += 1;
|
|
} while ([RZFileManager() fileExistsAtPath:filePath]);
|
|
|
|
self.filename = filePath.lastPathComponent;
|
|
}
|
|
|
|
- (BOOL)openFileHandle
|
|
{
|
|
NSString *filePath = self.filePath;
|
|
|
|
if (self.isSender == NO && self.isResume == NO) {
|
|
[self setNonexistentFilename];
|
|
|
|
[RZFileManager() createFileAtPath:filePath contents:[NSData data] attributes:nil];
|
|
}
|
|
|
|
NSFileHandle *fileHandle = [NSFileHandle fileHandleForUpdatingAtPath:filePath];
|
|
|
|
if (fileHandle == nil) {
|
|
[self closeWithLocalizedError:@"TDCFileTransferDialog[nab-dx]"];
|
|
|
|
return NO;
|
|
}
|
|
|
|
if (self.isResume) {
|
|
[fileHandle seekToFileOffset:self.processedFilesize];
|
|
}
|
|
|
|
self.fileHandle = fileHandle;
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)closeFileHandle
|
|
{
|
|
if (self.fileHandle == nil) {
|
|
return;
|
|
}
|
|
|
|
[self.fileHandle closeFile];
|
|
|
|
self.fileHandle = nil;
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Socket Delegate
|
|
|
|
- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
|
|
{
|
|
if (self.isActingAsServer == NO) {
|
|
return;
|
|
}
|
|
|
|
if (self.listeningServerConnectedClient == nil) {
|
|
self.listeningServerConnectedClient = newSocket;
|
|
} else {
|
|
[newSocket disconnect];
|
|
|
|
return;
|
|
}
|
|
|
|
if (self.isReversed) {
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusReceiving;
|
|
} else {
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusSending;
|
|
}
|
|
|
|
[self.transferDialog updateMaintenanceTimer];
|
|
|
|
if ([self openFileHandle] == NO) {
|
|
return;
|
|
}
|
|
|
|
if (self.isReversed) {
|
|
[self.readSocket readDataWithTimeout:(-1) tag:0];
|
|
} else {
|
|
[self send];
|
|
}
|
|
}
|
|
|
|
- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
|
|
{
|
|
if (self.isActingAsClient == NO) {
|
|
return;
|
|
}
|
|
|
|
if (self.isReversed == NO) {
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusReceiving;
|
|
} else {
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusSending;
|
|
}
|
|
|
|
[self.transferDialog updateMaintenanceTimer];
|
|
|
|
if ([self openFileHandle] == NO) {
|
|
return;
|
|
}
|
|
|
|
if (self.isReversed == NO) {
|
|
[self.readSocket readDataWithTimeout:(-1) tag:0];
|
|
} else {
|
|
[self send];
|
|
}
|
|
}
|
|
|
|
- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)error
|
|
{
|
|
if (self.transferStatus == TDCFileTransferDialogTransferStatusComplete ||
|
|
self.transferStatus == TDCFileTransferDialogTransferStatusFatalError ||
|
|
self.transferStatus == TDCFileTransferDialogTransferStatusRecoverableError)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (error) {
|
|
[self closeWithLocalizedError:@"TDCFileTransferDialog[s79-3a]" description:error.localizedDescription];
|
|
} else {
|
|
[self close];
|
|
}
|
|
}
|
|
|
|
- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
|
|
{
|
|
if (self.isSender != NO) {
|
|
return;
|
|
}
|
|
|
|
/* Update progress */
|
|
self.currentRecord += data.length;
|
|
|
|
self.processedFilesize += data.length;
|
|
|
|
if (data.length > 0) {
|
|
@try {
|
|
[self.fileHandle writeData:data];
|
|
}
|
|
@catch (NSException *exception) {
|
|
LogToConsoleError("Caught exception: %{public}@", exception.reason);
|
|
LogStackTrace();
|
|
|
|
if ([exception.reason contains:@"No space left on device"]) {
|
|
[self failWithNoSpaceLeftOnDevice];
|
|
|
|
return;
|
|
}
|
|
|
|
[self closeWithLocalizedError:TXTLS(@"TDCFileTransferDialog[05g-c8]")];
|
|
|
|
return;
|
|
} // @catch
|
|
}
|
|
|
|
/* Send acknowledgement back to server */
|
|
uint32_t processedFilesize = (self.processedFilesize & 0xFFFFFFFF);
|
|
|
|
unsigned char ackPacket[4];
|
|
|
|
ackPacket[0] = ((processedFilesize >> 24) & 0xFF);
|
|
ackPacket[1] = ((processedFilesize >> 16) & 0xFF);
|
|
ackPacket[2] = ((processedFilesize >> 8) & 0xFF);
|
|
ackPacket[3] = (processedFilesize & 0xFF);
|
|
|
|
NSData *ackPacketData = [NSData dataWithBytes:ackPacket length:4];
|
|
|
|
[self.readSocket writeData:ackPacketData withTimeout:(-1) tag:0];
|
|
|
|
/* Continue requesting data if the transfer is not complete */
|
|
if (self.processedFilesize < self.totalFilesize) {
|
|
[self.readSocket readDataWithTimeout:(-1) tag:0];
|
|
|
|
return;
|
|
}
|
|
|
|
/* Update status and tear down transfer */
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusComplete;
|
|
|
|
[self close];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Socket Write
|
|
|
|
- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
|
|
{
|
|
if (self.isSender == NO) {
|
|
return;
|
|
}
|
|
|
|
/* Acknowledge sent data */
|
|
self.sendQueueSize -= 1;
|
|
|
|
/* Continue sending data if the transfer is not complete */
|
|
if (self.processedFilesize < self.totalFilesize) {
|
|
[self send];
|
|
|
|
return;
|
|
}
|
|
|
|
/* Wait until the send queue has cleared before closing
|
|
connection. Just because we finished processing the
|
|
data here doesn't mean it has been sent yet. */
|
|
if (self.sendQueueSize > 0) {
|
|
return;
|
|
}
|
|
|
|
/* Update status and tear down transfer */
|
|
self.transferStatus = TDCFileTransferDialogTransferStatusComplete;
|
|
|
|
[self close];
|
|
}
|
|
|
|
- (void)send
|
|
{
|
|
if (self.isSender == NO) {
|
|
return;
|
|
}
|
|
|
|
if (self.transferStatus != TDCFileTransferDialogTransferStatusSending) {
|
|
return;
|
|
}
|
|
|
|
do {
|
|
if (self.currentRecord >= RATE_LIMIT) {
|
|
return;
|
|
}
|
|
|
|
if (self.sendQueueSize >= MAX_QUEUE_SIZE) {
|
|
return;
|
|
}
|
|
|
|
if (self.processedFilesize >= self.totalFilesize) {
|
|
return;
|
|
}
|
|
|
|
NSData *dataToWrite = [self.fileHandle readDataOfLength:BUFFER_SIZE];
|
|
|
|
self.currentRecord += dataToWrite.length;
|
|
|
|
self.processedFilesize += dataToWrite.length;
|
|
|
|
self.sendQueueSize += 1;
|
|
|
|
[self.writeSocket writeData:dataToWrite withTimeout:_sendDataTimeout tag:0];
|
|
} while (1);
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Actions
|
|
|
|
- (void)updateClearButton
|
|
{
|
|
[self.transferDialog updateClearButton];
|
|
}
|
|
|
|
- (void)reloadStatusInformation
|
|
{
|
|
TDCFileTransferDialogTableCell *transferTableCell = self.transferTableCell;
|
|
|
|
if (transferTableCell == nil) {
|
|
return;
|
|
}
|
|
|
|
[transferTableCell reloadStatusInformation];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Properties
|
|
|
|
- (nullable GCDAsyncSocket *)writeSocket
|
|
{
|
|
if (self.isReversed) {
|
|
return self.connectionToRemoteServer;
|
|
} else {
|
|
return self.listeningServerConnectedClient;
|
|
}
|
|
}
|
|
|
|
- (nullable GCDAsyncSocket *)readSocket
|
|
{
|
|
if (self.isReversed) {
|
|
return self.listeningServerConnectedClient;
|
|
} else {
|
|
return self.connectionToRemoteServer;
|
|
}
|
|
}
|
|
|
|
- (BOOL)isActingAsServer
|
|
{
|
|
return ((self.isSender && self.isReversed == NO) ||
|
|
(self.isSender == NO && self.isReversed));
|
|
}
|
|
|
|
- (BOOL)isActingAsClient
|
|
{
|
|
return ((self.isSender == NO && self.isReversed == NO) ||
|
|
(self.isSender && self.isReversed));
|
|
}
|
|
|
|
- (TDCFileTransferDialog *)transferDialog
|
|
{
|
|
return [TXSharedApplication sharedFileTransferDialog];
|
|
}
|
|
|
|
- (NSArray<NSNumber *> *)speedRecords
|
|
{
|
|
@synchronized (self.speedRecordsPrivate) {
|
|
return [self.speedRecordsPrivate copy];
|
|
}
|
|
}
|
|
|
|
- (nullable NSString *)filePath
|
|
{
|
|
NSString *path = self.path;
|
|
|
|
NSString *filename = self.filename;
|
|
|
|
if (path == nil || filename == nil) {
|
|
return nil;
|
|
}
|
|
|
|
return [path stringByAppendingPathComponent:filename];
|
|
}
|
|
|
|
- (nullable NSURL *)fileURL
|
|
{
|
|
NSString *filePath = self.filePath;
|
|
|
|
if (filePath == nil) {
|
|
return nil;
|
|
}
|
|
|
|
return [NSURL fileURLWithPath:filePath];
|
|
}
|
|
|
|
- (uint64_t)currentFilesize
|
|
{
|
|
NSString *filePath = self.filePath;
|
|
|
|
if (filePath == nil) {
|
|
return 0;
|
|
}
|
|
|
|
if ([RZFileManager() fileExistsAtPath:filePath] == NO) {
|
|
return 0;
|
|
}
|
|
|
|
NSDictionary *fileAttributes = [RZFileManager() attributesOfItemAtPath:filePath error:NULL];
|
|
|
|
return fileAttributes.fileSize;
|
|
}
|
|
|
|
- (void)setTransferStatus:(TDCFileTransferDialogTransferStatus)transferStatus
|
|
{
|
|
if (self->_transferStatus != transferStatus) {
|
|
self->_transferStatus = transferStatus;
|
|
|
|
[self reloadStatusInformation];
|
|
}
|
|
}
|
|
|
|
- (void)resetProperties
|
|
{
|
|
if (self.isResume == NO) {
|
|
self.processedFilesize = 0;
|
|
}
|
|
|
|
self.currentRecord = 0;
|
|
|
|
self.errorMessageDescription = nil;
|
|
|
|
self.sendQueueSize = 0;
|
|
|
|
@synchronized(self.speedRecordsPrivate) {
|
|
[self.speedRecordsPrivate removeAllObjects];
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|