Reliable File Transfers with Openfire File Transfer Proxy Service

In my previous post I described how I was able to fix a configuration error at the server to make the File Transfer Proxy Service of Openfire work. I was able to successfully send and receive files using Jon Staff’s XMPP File Transfer Demo (Download it from Github here).

Not long after this did I realize that there were more problems to solve.

I logged in using Adium as one user and ran the xmpp-file-transfer-demo as the other user. Sending and receiving files worked both ways to and from each client. I could send pdf files as large as 5MB. But, wait a minute, a 5MB file transfer finishing in less than 3 seconds? How can that be with the 1 MBps internet connection I’m using?

The reason is that both clients were on the same network, and that by default a receiving client will try to use a direct connection first before attempting to connect to a proxy. I wanted to test my openfire proxy so I had to force my receiving client to connect to the proxy service. So what I did was to edit the attemptStreamhostsConnection: method inside XMPPIncomingFileTransfer.m :


- (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"];

    // get the jid attribute
    NSString *jid = [streamhost attributeStringValueForName:@"jid"];
    // ignore this streamhost if it refers to a direct connection to the client
    if ([jid containsString:@"@"]) {
      continue;
    }

    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;
    }

    // ... other code omitted for brevity

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

Specifically, this is what was added:


    // get the jid attribute
    NSString *jid = [streamhost attributeStringValueForName:@"jid"];
    // ignore this streamhost if it refers to a direct connection to the client
    if ([jid containsString:@"@"]) {
      continue;
    }

Basically, what it does is it checks if the current streamhost candidate is the client itself by reading the jid attribute of the XML element and checking for the presence of a ‘@’ character. This works because the jid of a client is always of the form @ whereas the jid of a proxy streamhost is of the form proxy. and contains no ‘@’ character.

After adding this code, attemptStreamhostsConnection now always connects only to the proxy streamhost and finally I can test my Openfire proxy.

Here’s where I get stumped again. I could transfer small files (up to 100K) easily. But anything from 200K to 500K will sometimes succeed but more often fail with the error: Incoming file transfer failed because: Socket disconnected before transfer complete.

Eventually I traced the problem by turning on debugging in GCDAsyncSocket (GCDAsyncSocket.m) class in order to find out what’s happening:


#ifndef GCDAsyncSocketLoggingEnabled
#define GCDAsyncSocketLoggingEnabled 1
#endif

Running the test again, the cause of the problem now became obvious:

2015-04-25 10:03:44:967 FileTransferDemo[3925:791f] GCDAsyncSocket: doReadData
2015-04-25 10:03:44:967 FileTransferDemo[3925:791f] GCDAsyncSocket: read from socket = 1448
2015-04-25 10:03:45:030 FileTransferDemo[3925:561b] GCDAsyncSocket: ReadTimeout
2015-04-25 10:03:45:030 FileTransferDemo[3925:561b] GCDAsyncSocket: closeWithError:
2015-04-25 10:03:45:030 FileTransferDemo[3925:561b] GCDAsyncSocket: endConnectTimeout
2015-04-25 10:03:45:030 FileTransferDemo[3925:561b] GCDAsyncSocket: removeStreamsFromRunLoop
2015-04-25 10:03:45:030 FileTransferDemo[3925:561b] GCDAsyncSocket: Removing streams from runloop...
2015-04-25 10:03:45:030 FileTransferDemo[3925:5f53] GCDAsyncSocket: unscheduleCFStreams:
2015-04-25 10:03:45:030 FileTransferDemo[3925:561b] GCDAsyncSocket: stopCFStreamThreadIfNeeded

GCDAsyncSocket was closing the connection whenever it encounters a timeout on reading a block of incoming data. Fortunately the GCDAsyncSocketDelegate protocol defines a method that can remedy this situation:


/**
 * Called if a read operation has reached its timeout without completing.
 * This method allows you to optionally extend the timeout.
 * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount.
 * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual.
 * 
 * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method.
 * The length parameter is the number of bytes that have been read so far for the read operation.
 * 
 * Note that this method may be called multiple times for a single read if you return positive numbers.
**/
- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag elapsed (NSTimeInterval)elapsed bytesDone:(NSUInteger)length;

The solution then was simple. Implement this delegate method inside XMPPIncomingFileTransfers.m . I put it right after the socketDidDisconnect:withError: method just before the #pragma mark - SOCKS5 line:


- (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag
                 elapsed:(NSTimeInterval)elapsed
               bytesDone:(NSUInteger)length
{
    if (elapsed < 10.0)
        return 1.0;
    else
        return 0.0;
}

What happens is that when GCDAsyncSocket encounters a read timeout event, it calls this delegate method first and if it gets a positive number ( in this case the method returns 1.0, which means 1 second), it will extend the timeout value by this amount and attempt another read. Without this value, it always closes the connection on the first read timeout event. Note the check for elapsed time in the first line. Since this method may be called multiple times for a single read with the timeout increasing with each read, we have to give up at some point when the timeout becomes too large. For all we know the server may already be totally inaccessible and our client would then be waiting forever! I arbitrarily decided on 10 seconds but may adjust it later to a more reasonable value as I continue my testing.

So after implementing this method I could now reliably transfer files as large as 10MB. This is my first foray digging into the framework code. If anyone has a better solution feel free to leave a comment!

How to configure FileTransferProxy Service of Openfire to work with XMPPFramework

I’m currently working on an iOS chat application using Openfire as my XMPP server. On the client side I used the excellent XMPPFramework by Robbie Hanson. It may look daunting at first because of its huge size and extensive coverage of the XMPP specifications, but overall, I found it relatively straightforward to use.

Anyway, my app was at the stage where users can register and send/receive messages to each other. The next hurdle was sending and receiving binary files. This is where I hit a stumbling block that cost me a whole 3 days to get over.

The problem was that the app was receiving this iq stanza that’s supposed to contain all possible streamhosts that the xmpp client can connect to initiate the file transfer:


<iq xmlns="jabber:client" type="set" to="alice@xmpp.example.com/b" id="E0FEE3F8-7027-4456-9E48-7285F0C3A764" from="bob@xmpp.example.com/a">
    <query xmlns="http://jabber.org/protocol/bytestreams" sid="4E26A21E-915E-4C1E-9ABD-60392FF78FAB">
        <streamhost jid="proxy.xmpp.example.com" port="7777"/>
    </query>
</iq>

Notice this element is missing the host=”…” attribute:


<streamhost jid="proxy.xmpp.example.com" port="7777"/>

It turns out that the problem was at the XMPP server side. Fortunately I decided to take a look at Openfire’s error log:


2015.04.22 23:05:49 org.jivesoftware.openfire.filetransfer.proxy.FileTransferProxy - Couldn't discover local host
java.net.UnknownHostException: inodev20: inodev20: Name or service not known

And therein lies the problem. Openfire was obtaining the server’s ip address by getting the hostname and doing a DNS lookup. Since my hostname is not a fully qualified domain name, it couldn’t get the ip address.

The solution was to change the hostname to a real domain name that can be resolved through DNS. Thus I changed it rightfully so to our own domain name:


nano /etc/sysconfig/network

NETWORKING=yes
HOSTNAME=xmpp.example.com

Restart the network, and restart openfire:


/etc/init.d/network restart
service openfire restart

After that everything was well!


<streamhost jid="proxy.xmpp.example.com" host="204.122.x.x" port="7777"/>

My file sending and receiving is finally working!

Changing rootViewController in AppDelegate

In a storyboard-based app (which is now the default and only choice as of Xcode 6), the root view controller is already set behind the scenes by the time application:didFinishLaunchingWithOptions: is called on the AppDelegate. We can get the current rootViewController anytime inside this method:


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UIViewController *rootViewController = self.window.rootViewController;
. . .

}

What if we want to present a different root view controller on startup? For example we want to present a signup form if a user is not yet registered but go straight to the main view otherwise.

It is fairly straightforward to this without much complexity in storyboard. It turns out that every view controller holds a reference to the UIStoryboard instance that created it through its storyboard property, and the root view controller “is” an instance of UIViewController (If an app doesn’t use storyboards, this of course would be set to nil).


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UIViewController *rootViewController = self.window.rootViewController;
UIStoryboard *storyboard = rootViewController.storyboard;
...
}

We can now instantiate any viewController within our Storyboard if we know its storyboardID:
storyboardID


UIViewController *mySampleViewController = [storyboard instantiateViewControllerWithIdentifier:@"myViewController"];

All we need to do now is set this as our new rootViewController:

self.window.rootViewController = mysampleViewController
Here’s how we can use this in our app:


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSString userName = [[NSUserDefaults standardDefaults] stringForKey:@"USERNAME"];
// Our assumption is that if we have a username saved, then this user has already registered before
if (userName == nil) {
UIViewController *rootViewController = self.window.rootViewController;
UIStoryboard *storyboard = rootViewController.storyboard;
UIViewController *mySampleViewController = [storyboard instantiateViewControllerWithIdentifier:@"myViewController"];
self.window.rootViewController = mySampleViewController;
}
// of course, there's nothing to do here but return if we are already registered
// since our root view controller is already set to the main view controller by our storyboard
return YES;
}