//
// Created by Jonathon Staff on 10/21/14.
// Copyright (c) 2014 nplexity, LLC. All rights reserved.
//

#if !__has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif

#import "XMPPIncomingFileTransfer.h"
#import "XMPPConstants.h"
#import "XMPPLogging.h"
#import "idn-int.h"
#import "NSNumber+XMPP.h"
#import "NSData+XMPP.h"

#if DEBUG
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // XMPP_LOG_LEVEL_VERBOSE | XMPP_LOG_FLAG_TRACE;
#else
    static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
#endif

/**
* Tags for _asyncSocket handling.
*/
#define SOCKS_TAG_WRITE_METHOD 101
#define SOCKS_TAG_READ_METHOD 102
#define SOCKS_TAG_WRITE_CONNECT 103
#define SOCKS_TAG_READ_REPLY 104
#define SOCKS_TAG_READ_ADDRESS 105
#define SOCKS_TAG_READ_DATA 106

#define TIMEOUT_WRITE -1
#define TIMEOUT_READ 5.0

// XMPP Incoming File Transfer State
typedef NS_ENUM(int, XMPPIFTState) {
  XMPPIFTStateNone,
  XMPPIFTStateWaitingForSIOffer,
  XMPPIFTStateWaitingForStreamhosts,
  XMPPIFTStateConnectingToStreamhosts,
  XMPPIFTStateConnected,
  XMPPIFTStateWaitingForIBBOpen,
  XMPPIFTStateWaitingForIBBData
};

NSString *const XMPPIncomingFileTransferErrorDomain = @"XMPPIncomingFileTransferErrorDomain";

@interface XMPPIncomingFileTransfer () {
  XMPPIFTState _transferState;

  XMPPJID *_senderJID;

  NSString *_streamhostsQueryId;
  NSString *_streamhostUsed;

  NSMutableData *_receivedData;
  NSString *_receivedFileName;
  NSUInteger _totalDataSize;
  NSUInteger _receivedDataSize;

  dispatch_source_t _ibbTimer;
}

@end

@implementation XMPPIncomingFileTransfer


#pragma mark - Lifecycle

- (instancetype)initWithDispatchQueue:(dispatch_queue_t)queue
{
  self = [super initWithDispatchQueue:queue];
  if (self) {
    _transferState = XMPPIFTStateNone;
  }
  return self;
}

/**
* Standard deconstructor.
*/
- (void)dealloc
{
  XMPPLogTrace();

  if (_transferState != XMPPIFTStateNone) {
    XMPPLogWarn(@"%@: Deallocating prior to completion or cancellation.", THIS_FILE);
  }

  if (_ibbTimer)
    dispatch_source_cancel(_ibbTimer);
#if !OS_OBJECT_USE_OBJC
  dispatch_release(_ibbTimer);
  #endif
  _ibbTimer = NULL;

  if (_asyncSocket.delegate == self) {
    [_asyncSocket setDelegate:nil delegateQueue:NULL];
    [_asyncSocket disconnect];
  }
}


#pragma mark - Public Methods

/**
* Public facing method for accepting a SI offer. If autoAcceptFileTransfers is
* set to YES, this method will do nothing, since the internal method is invoked
* automatically.
*
* @see sendSIOfferAcceptance:
*/
- (void)acceptSIOffer:(XMPPIQ *)offer
{
  XMPPLogTrace();

  if (!_autoAcceptFileTransfers) {
    [self sendSIOfferAcceptance:offer];
  }
}


#pragma mark - Private Methods

/**
* This method will send the device's identity in response to a `disco#info`
* query. In our case, we will send something close the following:
*
* <iq type="result"
*     id="purplea2da8fc9"
*     from="mephisto@sanctuary.org/kurast"
*     to="baal@sanctuary.org/worldstonechamber">
*   <query xmlns="http://jabber.org/protocol/disco#info">
*     <identity category="client" type="phone"/>
*       <feature var="http://jabber.org/protocol/si"/>
*       <feature var="http://jabber.org/protocol/si/profile/file-transfer"/>
*       <feature var="http://jabber.org/protocol/bytestreams"/>
*       <feature var="http://jabber.org/protocol/ibb"/>
*   </query>
* </iq>
*
* This tells the requester who they're dealing with and which transfer types
* we support. If there's a better way than hard-coding these values, I'm open
* to suggestions.
*/
- (void)sendIdentity:(XMPPIQ *)request
{
  XMPPLogTrace();

  dispatch_block_t block = ^{
      @autoreleasepool {
        XMPPIQ *iq = [XMPPIQ iqWithType:@"result"
                                     to:request.from
                              elementID:request.elementID];
        [iq addAttributeWithName:@"from" stringValue:xmppStream.myJID.full];

        NSXMLElement
            *query = [NSXMLElement elementWithName:@"query" xmlns:XMPPDiscoInfoNamespace];

        NSXMLElement *identity = [NSXMLElement elementWithName:@"identity"];
        [identity addAttributeWithName:@"category" stringValue:@"client"];
        [identity addAttributeWithName:@"type" stringValue:@"ios-osx"];
        [query addChild:identity];

        NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"];
        [feature addAttributeWithName:@"var" stringValue:XMPPSINamespace];
        [query addChild:feature];

        NSXMLElement *feature1 = [NSXMLElement elementWithName:@"feature"];
        [feature1 addAttributeWithName:@"var" stringValue:XMPPSIProfileFileTransferNamespace];
        [query addChild:feature1];

        if (!self.disableSOCKS5) {
          NSXMLElement *feature2 = [NSXMLElement elementWithName:@"feature"];
          [feature2 addAttributeWithName:@"var" stringValue:XMPPBytestreamsNamespace];
          [query addChild:feature2];
        }

        if (!self.disableIBB) {
          NSXMLElement *feature3 = [NSXMLElement elementWithName:@"feature"];
          [feature3 addAttributeWithName:@"var" stringValue:XMPPIBBNamespace];
          [query addChild:feature3];
        }

        [iq addChild:query];
        [xmppStream sendElement:iq];
      }
  };

  if (dispatch_get_specific(moduleQueueTag))
    block();
  else
    dispatch_async(moduleQueue, block);
}

/**
* This method will send an IQ stanza accepting the SI offer. We need to choose
* which 'stream-method' we prefer to use. For now, we will be using IBB as the
* 'stream-method', but SOCKS5 is preferable.
*
* Take a look at XEP-0096 Examples 2 and 4 for more details.
*/
- (void)sendSIOfferAcceptance:(XMPPIQ *)offer
{
  XMPPLogTrace();

  dispatch_block_t block = ^{
      @autoreleasepool {
        // Store the sender's JID
        _senderJID = offer.from;

        // Store the sid for later use
        NSXMLElement *inSi = offer.childElement;
        self.sid = [inSi attributeStringValueForName:@"id"];

        // Store the size of the incoming data for later use
        NSXMLElement *inFile = [inSi elementForName:@"file"];
        _totalDataSize = [inFile attributeUnsignedIntegerValueForName:@"size"];

        // Store the name of the file for later use
        _receivedFileName = [inFile attributeStringValueForName:@"name"];

        // Outgoing
        XMPPIQ *iq = [XMPPIQ iqWithType:@"result"
                                     to:offer.from
                              elementID:offer.elementID];


        NSXMLElement *si = [NSXMLElement elementWithName:@"si" xmlns:XMPPSINamespace];

        NSXMLElement *feature = [NSXMLElement elementWithName:@"feature"
                                                        xmlns:XMPPFeatureNegNamespace];

        NSXMLElement *x = [NSXMLElement elementWithName:@"x" xmlns:@"jabber:x:data"];
        [x addAttributeWithName:@"type" stringValue:@"submit"];

        NSXMLElement *field = [NSXMLElement elementWithName:@"field"];
        [field addAttributeWithName:@"var" stringValue:@"stream-method"];

        NSXMLElement *value = [NSXMLElement elementWithName:@"value"];

        // Prefer SOCKS5 if it's not disabled.
        if (!self.disableSOCKS5) {
          [value setStringValue:XMPPBytestreamsNamespace];
          _transferState = XMPPIFTStateWaitingForStreamhosts;
        } else {
          [value setStringValue:XMPPIBBNamespace];
          _transferState = XMPPIFTStateWaitingForIBBOpen;
        }

        [field addChild:value];
        [x addChild:field];
        [feature addChild:x];
        [si addChild:feature];
        [iq addChild:si];

        [xmppStream sendElement:iq];
      }
  };

  if (dispatch_get_specific(moduleQueueTag))
    block();
  else
    dispatch_async(moduleQueue, block);
}


#pragma mark - IBB Methods

/**
* This method will send an IQ stanza accepting the IBB request. See XEP-0047
* Example 2 for more details.
*/
- (void)sendIBBAcceptance:(XMPPIQ *)request
{
  XMPPLogTrace();

  dispatch_block_t block = ^{
      @autoreleasepool {
        XMPPIQ *iq = [XMPPIQ iqWithType:@"result"
                                     to:request.from
                              elementID:request.elementID];
        [xmppStream sendElement:iq];

        // Prepare to receive data
        _receivedData = [NSMutableData new];
      }
  };

  if (dispatch_get_specific(moduleQueueTag))
    block();
  else
    dispatch_async(moduleQueue, block);
}

/**
* This method is responsible for reading the incoming data from the IQ stanza
* and writing it to the member variable '_receivedData'. After successfully
* reading the data, a response (XEP-0047 Example 7) will be sent back to the
* sender.
*/
- (void)processReceivedIBBDataIQ:(XMPPIQ *)received
{
  XMPPLogTrace();

  dispatch_block_t block = ^{
      @autoreleasepool {
        // Handle the scenario that the transfer is cancelled.
        [self resetIBBTimer:20];

        // Handle incoming data
        NSXMLElement *dataElem = received.childElement;
        NSData
            *temp = [[NSData alloc] initWithBase64EncodedString:dataElem.stringValue options:0];
        [_receivedData appendData:temp];

        // According the base64 encoding, it takes up 4/3 n bytes of space, so
        // we need to find the size of the data before base64.
        _receivedDataSize += (3 * dataElem.stringValue.length) / 4;

        XMPPLogVerbose(@"Downloaded %lu/%lu bytes in IBB transfer.",
                       (unsigned long) _receivedDataSize, (unsigned long) _totalDataSize);

        if (_receivedDataSize < _totalDataSize) {
          // Send ack response
          XMPPIQ *iq = [XMPPIQ iqWithType:@"result"
                                       to:received.from
                                elementID:received.elementID];
          [xmppStream sendElement:iq];
        } else {
          // We're finished!
          XMPPLogInfo(@"Finished downloading IBB data.");
          [self transferSuccess];
        }
      }
  };

  if (dispatch_get_specific(moduleQueueTag))
    block();
  else
    dispatch_async(moduleQueue, block);
}


#pragma mark - Util Methods

/**
* This method determines whether or not the IQ stanza is a `disco#info`
* request. Should be in the following form:
*
* <iq xmlns="jabber:client"
*     from="baal@sanctuary.org/worldstonechamber"
*     to="mephisto@sanctuary.org/kurast"
*     type="get"
*     id="purplea2da8fc9">
*   <query xmlns="http://jabber.org/protocol/disco#info"/>
* </iq>
*/
- (BOOL)isDiscoInfoIQ:(XMPPIQ *)iq
{
  if (!iq) return NO;
  NSXMLElement *query = iq.childElement;
  return query != nil && [query.xmlns isEqualToString:XMPPDiscoInfoNamespace];
}

/**
* This method determines whether or not the the IQ stanza is a Stream
* Initiation Offer (XEP-0096 Examples 1 and 3).
*/
- (BOOL)isSIOfferIQ:(XMPPIQ *)iq
{
  if (!iq) return NO;
  if (![iq.type isEqualToString:@"set"]) return NO;

  NSXMLElement *si = iq.childElement;
  if (!si || ![si.xmlns isEqualToString:XMPPSINamespace]) return NO;

  NSXMLElement *file = (DDXMLElement *) [si childAtIndex:0];
  if (!file || ![file.xmlns isEqualToString:XMPPSIProfileFileTransferNamespace]) return NO;

  NSXMLElement *feature = (DDXMLElement *) [si childAtIndex:1];
  return !(!feature || ![feature.xmlns isEqualToString:XMPPFeatureNegNamespace]);

  // Maybe there should be further verification, but I think this should be
  // plenty...
}

/**
* This method determines whether or not the IQ stanza is an IBB session request
* (XEP-0047 Example 1).
*/
- (BOOL)isIBBOpenRequestIQ:(XMPPIQ *)iq
{
  if (!iq) return NO;
  if (![iq.type isEqualToString:@"set"]) return NO;

  NSXMLElement *open = iq.childElement;
  return !(!open || ![open.xmlns isEqualToString:XMPPIBBNamespace]);
}

/**
* This method determines whether or not the IQ stanza is an IBB data stanza
* (XEP-0047 Example 6).
*/
- (BOOL)isIBBDataIQ:(XMPPIQ *)iq
{
  if (!iq) return NO;
  if (![iq.type isEqualToString:@"set"]) return NO;

  NSXMLElement *data = iq.childElement;
  return !(!data || ![data.xmlns isEqualToString:XMPPIBBNamespace]);
}

/**
* This method determines whether or not the IQ stanza contains a list of
* streamhosts as shown in XEP-0065 Example 12.
*/
- (BOOL)isStreamhostsListIQ:(XMPPIQ *)iq
{
  if (!iq) return NO;
  if (![iq.type isEqualToString:@"set"]) return NO;

  NSXMLElement *query = iq.childElement;
  if (!query || ![[query attributeStringValueForName:@"sid"] isEqualToString:self.sid]) return NO;

  return [query elementsForName:@"streamhost"].count > 0;
}

/**
* This method returns the SHA1 hash as per XEP-0065.
*
* The [address] MUST be SHA1(SID + Initiator JID + Target JID) and the output
* is hexadecimal encoded (not binary).
*
* Because this is an incoming file transfer, we are always the target.
*/
- (NSData *)sha1Hash
{
  NSString *hashMe =
      [NSString stringWithFormat:@"%@%@%@", self.sid, _senderJID.full, xmppStream.myJID.full];
  NSData *hashRaw = [[hashMe dataUsingEncoding:NSUTF8StringEncoding] xmpp_sha1Digest];
  NSData *hash = [[hashRaw xmpp_hexStringValue] dataUsingEncoding:NSUTF8StringEncoding];

  XMPPLogVerbose(@"%@: hashMe : %@", THIS_FILE, hashMe);
  XMPPLogVerbose(@"%@: hashRaw: %@", THIS_FILE, hashRaw);
  XMPPLogVerbose(@"%@: hash   : %@", THIS_FILE, hash);

  return hash;
}

/**
* This method is called to clean up everything when the transfer fails.
*/
- (void)failWithReason:(NSString *)causeOfFailure
                 error:
                     (NSError *)error
{
  XMPPLogTrace();
  XMPPLogInfo(@"Incoming file transfer failed because: %@", causeOfFailure);

  if (!error && causeOfFailure) {
    NSDictionary *errInfo = @{NSLocalizedDescriptionKey : causeOfFailure};
    error = [NSError errorWithDomain:XMPPIncomingFileTransferErrorDomain
                                code:-1
                            userInfo:errInfo];
  }

  _transferState = XMPPIFTStateNone;
  [multicastDelegate xmppIncomingFileTransfer:self didFailWithError:error];
}

/**
* This method is called when the transfer is successfully completed. It
* handles resetting variables for another transfer and alerts the delegate of
* the transfer completion.
*/
- (void)transferSuccess
{
  XMPPLogTrace();

  dispatch_block_t block = ^{
      @autoreleasepool {
        [self cancelIBBTimer];

        [multicastDelegate xmppIncomingFileTransfer:self
                                 didSucceedWithData:_receivedData
                                              named:_receivedFileName];
        [self cleanUp];
      }
  };

  if (dispatch_get_specific(moduleQueueTag))
    block();
  else
    dispatch_async(moduleQueue, block);
}

/**
* This method is used to reset the system for receiving new files.
*/
- (void)cleanUp
{
  XMPPLogTrace();

  if (_asyncSocket) {
    [_asyncSocket setDelegate:nil];
    [_asyncSocket disconnect];
    _asyncSocket = nil;
  }

  _streamMethods &= 0;
  _transferState = XMPPIFTStateNone;
  _senderJID = nil;
  _streamhostsQueryId = nil;
  _streamhostUsed = nil;
  _receivedData = nil;
  _receivedFileName = nil;
  _totalDataSize = 0;
  _receivedDataSize = 0;
}


#pragma mark - Timeouts

/**
* Resets the IBB timer that will cause the transfer to formally fail if an IBB
* data IQ stanza isn't received within the timeout.
*/
- (void)resetIBBTimer:(NSTimeInterval)timeout
{
  NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue.");

  if (_ibbTimer == NULL) {
    _ibbTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, moduleQueue);

    dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC);

    dispatch_source_set_timer(_ibbTimer, tt, DISPATCH_TIME_FOREVER, 1);
    dispatch_resume(_ibbTimer);
  } else {
    dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC);
    dispatch_source_set_timer(_ibbTimer, tt, DISPATCH_TIME_FOREVER, 1);
  }

  dispatch_source_set_event_handler(_ibbTimer, ^{
      @autoreleasepool {
        NSString *errMsg = @"The IBB transfer timed out. It's likely that the sender canceled the"
            @" transfer or has gone offline.";
        [self failWithReason:errMsg error:nil];
      }
  });
}

- (void)cancelIBBTimer
{
  NSAssert(dispatch_get_specific(moduleQueueTag), @"Invoked on incorrect queue.");

  if (_ibbTimer) {
    dispatch_source_cancel(_ibbTimer);
#if !OS_OBJECT_USE_OBJC
    dispatch_release(_ibbTimer);
    #endif
    _ibbTimer = NULL;
  }
}


#pragma mark - XMPPStreamDelegate

- (BOOL)xmppStream:(XMPPStream *)sender didReceiveIQ:(XMPPIQ *)iq
{
  if (_transferState == XMPPIFTStateNone && [self isDiscoInfoIQ:iq]) {
    [self sendIdentity:iq];
    _transferState = XMPPIFTStateWaitingForSIOffer;
    return YES;
  }

  if ((_transferState == XMPPIFTStateNone || _transferState == XMPPIFTStateWaitingForSIOffer)
      && [self isSIOfferIQ:iq]) {
    // Alert the delegate that we've received a stream initiation offer
    [multicastDelegate xmppIncomingFileTransfer:self didReceiveSIOffer:iq];

    if (_autoAcceptFileTransfers) {
      [self sendSIOfferAcceptance:iq];
    }

    return YES;
  }

  if (_transferState == XMPPIFTStateWaitingForStreamhosts && [self isStreamhostsListIQ:iq]) {
    [self attemptStreamhostsConnection:iq];
    return YES;
  }

  if (_transferState == XMPPIFTStateWaitingForIBBOpen && [self isIBBOpenRequestIQ:iq]) {
    [self sendIBBAcceptance:iq];
    _transferState = XMPPIFTStateWaitingForIBBData;

    // Handle the scenario that the transfer is cancelled.
    [self resetIBBTimer:20];
    return YES;
  }

  if (_transferState == XMPPIFTStateWaitingForIBBData && [self isIBBDataIQ:iq]) {
    [self processReceivedIBBDataIQ:iq];
  }

  return iq != nil;
}


#pragma mark - GCDAsyncSocketDelegate

- (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port
{
  XMPPLogVerbose(@"%@: didConnectToHost:%@ port:%d", THIS_FILE, host, port);

  [self socks5WriteMethod];
  _transferState = XMPPIFTStateConnected;
}

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
  XMPPLogVerbose(@"%@: didReadData:%@ withTag:%ld", THIS_FILE, data, tag);

  switch (tag) {
    case SOCKS_TAG_READ_METHOD:
      [self socks5ReadMethod:data];
      break;
    case SOCKS_TAG_READ_REPLY:
      [self socks5ReadReply:data];
    case SOCKS_TAG_READ_ADDRESS:
      [_asyncSocket readDataToLength:_totalDataSize
                         withTimeout:TIMEOUT_READ
                                 tag:SOCKS_TAG_READ_DATA];
      break;
    case SOCKS_TAG_READ_DATA:
      // Success!
      _receivedData = [data mutableCopy];
      [self transferSuccess];
    default:
      break;
  }
}

- (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag
{
  XMPPLogVerbose(@"%@: didWriteDataWithTag:%ld", THIS_FILE, tag);

  switch (tag) {
    case SOCKS_TAG_WRITE_METHOD:
      [_asyncSocket readDataToLength:2 withTimeout:TIMEOUT_READ tag:SOCKS_TAG_READ_METHOD];
      break;
    case SOCKS_TAG_WRITE_CONNECT:
      [_asyncSocket readDataToLength:5 withTimeout:TIMEOUT_READ
                                 tag:SOCKS_TAG_READ_REPLY];
    default:
      break;
  }
}

- (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err
{
  XMPPLogTrace();

  if (_transferState == XMPPIFTStateConnected) {
    [self failWithReason:@"Socket disconnected before transfer complete." error:nil];
  }
}


#pragma mark - SOCKS5

/**
* This method attempts a connection to each of the streamhosts provided until
* either a connection is established or there are no more streamhosts. In the
* latter case, an error stanza is sent to the sender.
*
* @see socket:didConnectToHost:port:
*/
- (void)attemptStreamhostsConnection:(XMPPIQ *)iq
{
  XMPPLogTrace();

  dispatch_block_t block = ^{
      @autoreleasepool {
        _streamhostsQueryId = iq.elementID;
        _transferState = XMPPIFTStateConnectingToStreamhosts;
        _asyncSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:moduleQueue];

        // Since we've already validated our IQ stanza, we can just pull the data
        NSArray *streamhosts = [iq.childElement elementsForName:@"streamhost"];

        for (NSXMLElement *streamhost in streamhosts) {
          NSString *host = [streamhost attributeStringValueForName:@"host"];
          uint16_t port = (gl_uint16_t) [streamhost attributeUInt32ValueForName:@"port"];

          NSError *err;
          if (![_asyncSocket connectToHost:host onPort:port error:&err]) {
            XMPPLogVerbose(@"%@: Unable to host:%@ port:%d error:%@", THIS_FILE, host, port, err);
            continue;
          }

          // If we make it this far, we've successfully connected to one of the hosts.
          _streamhostUsed = [streamhost attributeStringValueForName:@"jid"];

          return;
        }

        // If we reach this, we weren't able to connect to any of the streamhosts.
        // We'll send an error to the sender to let them know, and then we'll alert
        // the delegate of the failure.
        //
        // XEP-0065 Example 13.
        //
        //  <iq from='mephisto@sanctuary.org/kurast'
        //      id='hu3vax16'
        //      to='baal@sanctuary.org/worldstonechamber'
        //      type='error'>
        //    <error type='modify'>
        //      <not-acceptable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
        //    </error>
        //  </iq>

        XMPPIQ *errorIq = [XMPPIQ iqWithType:@"error" to:iq.from elementID:iq.elementID];

        NSXMLElement *errorElem = [NSXMLElement elementWithName:@"error"];
        [errorElem addAttributeWithName:@"type" stringValue:@"modify"];

        NSXMLElement *notAcceptable = [NSXMLElement elementWithName:@"not-acceptable"
                                                              xmlns:@"urn:ietf:params:xml:ns:xmpp-stanzas"];
        [errorElem addChild:notAcceptable];
        [errorIq addChild:errorElem];

        [xmppStream sendElement:errorIq];

        NSString *errMsg = @"Unable to connect to any of the provided streamhosts.";
        [self failWithReason:errMsg error:nil];
      }
  };

  if (dispatch_get_specific(moduleQueueTag))
    block();
  else
    dispatch_async(moduleQueue, block);
}

- (void)socks5WriteMethod
{
  XMPPLogTrace();

  dispatch_block_t block = ^{
      @autoreleasepool {
        // We will attempt anonymous authentication with the proxy. The request is
        // the same that we would read if this were a direct connection. The only
        // difference is this time we initiate the request as a client rather than
        // being a the 'server.'
        //
        //                 +----+----------+----------+
        //                 |VER | NMETHODS | METHODS  |
        //                 +----+----------+----------+
        //                 | 1  |    1     | 1 to 255 |
        //                 +----+----------+----------+
        //
        // We're sending:
        //
        // VER      = 5 (SOCKS5)
        // NMETHODS = 1 (number of methods)
        // METHODS  = 0 (no authentication)

        void *byteBuf = malloc(3);

        UInt8 ver = 5;
        memcpy(byteBuf, &ver, sizeof(ver));

        UInt8 nmethods = 1;
        memcpy(byteBuf + 1, &nmethods, sizeof(nmethods));

        UInt8 methods = 0;
        memcpy(byteBuf + 2, &methods, sizeof(methods));

        NSData *data = [NSData dataWithBytesNoCopy:byteBuf length:3 freeWhenDone:YES];
        [_asyncSocket writeData:data withTimeout:TIMEOUT_WRITE tag:SOCKS_TAG_WRITE_METHOD];
      }
  };

  if (dispatch_get_specific(moduleQueueTag))
    block();
  else
    dispatch_async(moduleQueue, block);
}

- (void)socks5ReadMethod:(NSData *)incomingData
{
  XMPPLogTrace();

  dispatch_block_t block = ^{
      @autoreleasepool {
        // We've sent a request to connect with no authentication. This is the
        // response:
        //
        //                    +----+--------+
        //                    |VER | METHOD |
        //                    +----+--------+
        //                    | 1  |   1    |
        //                    +----+--------+
        //
        // We're expecting:
        //
        // VER    = 5 (SOCKS5)
        // METHOD = 0 (no authentication)

        UInt8 version = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:0];
        UInt8 method = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:1];

        if (version != 5 || method) {
          [self failWithReason:@"Proxy doesn't allow anonymous authentication." error:nil];
          return;
        }

        NSData *hash = [self sha1Hash];

        //  The SOCKS request is formed as follows:
        //
        //       +----+-----+-------+------+----------+----------+
        //       |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
        //       +----+-----+-------+------+----------+----------+
        //       | 1  |  1  | X'00' |  1   | Variable |    2     |
        //       +----+-----+-------+------+----------+----------+
        //
        // We're sending:
        //
        // VER      = 5
        // CMD      = 1 (connect)
        // RSV      = 0 (reserved; this will always be 0)
        // ATYP     = 3 (domain name)
        // DST.ADDR (varies based on ATYP)
        // DST.PORT = 0 (according to XEP-0065)
        //
        // Immediately after ATYP, we need to send the length of our address. Because
        // SHA1 is always 40 bytes, we simply send this value. After it, we append
        // the actual hash and then the port.

        void *byteBuf = malloc(5 + 40 + 2);

        UInt8 ver = 5;
        memcpy(byteBuf, &ver, sizeof(ver));

        UInt8 cmd = 1;
        memcpy(byteBuf + 1, &cmd, sizeof(cmd));

        UInt8 rsv = 0;
        memcpy(byteBuf + 2, &rsv, sizeof(rsv));

        UInt8 atyp = 3;
        memcpy(byteBuf + 3, &atyp, sizeof(atyp));

        UInt8 hashlen = (UInt8) hash.length;
        memcpy(byteBuf + 4, &hashlen, sizeof(hashlen));

        memcpy(byteBuf + 5, hash.bytes, hashlen);

        UInt8 port = 0;
        memcpy(byteBuf + 5 + hashlen, &port, sizeof(port));
        memcpy(byteBuf + 6 + hashlen, &port, sizeof(port));

        NSData *data = [NSData dataWithBytesNoCopy:byteBuf length:47 freeWhenDone:YES];
        [_asyncSocket writeData:data withTimeout:TIMEOUT_WRITE tag:SOCKS_TAG_WRITE_CONNECT];

        XMPPLogVerbose(@"%@: writing connect request: %@", THIS_FILE, data);
      }
  };

  if (dispatch_get_specific(moduleQueueTag))
    block();
  else
    dispatch_async(moduleQueue, block);
}

- (void)socks5ReadReply:(NSData *)incomingData
{
  XMPPLogTrace();

  dispatch_block_t block = ^{
      @autoreleasepool {
        // The server/sender will reply to our connect command with the following:
        //
        //          +----+-----+-------+------+----------+----------+
        //          |VER | REP |  RSV  | ATYP | BND.ADDR | BND.PORT |
        //          +----+-----+-------+------+----------+----------+
        //          | 1  |  1  | X'00' |  1   | Variable |    2     |
        //          +----+-----+-------+------+----------+----------+
        //
        // VER = 5 (SOCKS5)
        // REP = 0 (Success)
        // RSV = 0
        // ATYP = 3 (Domain) - NOTE: Since we're using ATYP = 3, we must check the
        //                           length of the server's host in the next byte.

        UInt8 ver = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:0];
        UInt8 rep = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:1];
        UInt8 atyp = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:3];
        UInt8 hostlen = [NSNumber xmpp_extractUInt8FromData:incomingData atOffset:4];

        if (ver != 5 || rep || atyp != 3) {
          [self failWithReason:@"Invalid VER, REP, or ATYP." error:nil];
          return;
        }

        // According to XEP-0065 Example 23, we don't need to validate the
        // address we were sent (at least that is how I interpret it), so we
        // just read the next 42 bytes (hostlen + portlen) so there's no
        // conflict when reading the data and then send <streamhost-used/> to
        // the file transfer initiator. Note that the sid must be included.
        //
        // XEP-0065 Example 17:
        //
        //  <iq from='mephisto@sanctuary.org/kurast'
        //      id='hu3vax16'
        //      to='baal@sanctuary.org/worldstonechamber'
        //      type='result'>
        //    <query xmlns='http://jabber.org/protocol/bytestreams'
        //           sid='vxf9n471bn46'>
        //      <streamhost-used jid='baal@sanctuary.org/worldstonechamber'/>
        //    </query>
        //  </iq>

        XMPPIQ *iq = [XMPPIQ iqWithType:@"result" to:_senderJID elementID:_streamhostsQueryId];

        NSXMLElement *query = [NSXMLElement elementWithName:@"query"
                                                      xmlns:XMPPBytestreamsNamespace];
        [query addAttributeWithName:@"sid" stringValue:self.sid];

        NSXMLElement *streamhostUsed = [NSXMLElement elementWithName:@"streamhost-used"];
        [streamhostUsed addAttributeWithName:@"jid"
                                 stringValue:_streamhostUsed];

        [query addChild:streamhostUsed];
        [iq addChild:query];

        [xmppStream sendElement:iq];

        // We're basically piping these to dev/null because we don't care.
        // However, we need to tag this read so we can start to read the actual
        // data once this read is finished.
        [_asyncSocket readDataToLength:hostlen + 2
                           withTimeout:TIMEOUT_READ
                                   tag:SOCKS_TAG_READ_ADDRESS];
      }
  };

  if (dispatch_get_specific(moduleQueueTag))
    block();
  else
    dispatch_async(moduleQueue, block);
}

@end
