ObjXMPP  XMPPConnection.m at [abf66b5c9b]

File src/XMPPConnection.m artifact c9e6b14c30 part of check-in abf66b5c9b


/*
 * Copyright (c) 2010, 2011, 2012, 2013, 2015, 2016, 2017, 2018, 2019
 *   Jonathan Schleifer <js@heap.zone>
 * Copyright (c) 2011, 2012, Florian Zeitz <florob@babelmonkeys.de>
 *
 * https://heap.zone/objxmpp/
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice is present in all copies.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 COPYRIGHT OWNER 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.
 */

#include "config.h"

#define XMPP_CONNECTION_M

#include <assert.h>

#include <stringprep.h>
#include <idna.h>

#import <ObjOpenSSL/SSLSocket.h>
#import <ObjOpenSSL/SSLInvalidCertificateException.h>
#import <ObjOpenSSL/X509Certificate.h>

#import <ObjFW/OFInvalidArgumentException.h>

#import "XMPPConnection.h"
#import "XMPPANONYMOUSAuth.h"
#import "XMPPCallback.h"
#import "XMPPEXTERNALAuth.h"
#import "XMPPExceptions.h"
#import "XMPPIQ.h"
#import "XMPPJID.h"
#import "XMPPMessage.h"
#import "XMPPMulticastDelegate.h"
#import "XMPPPLAINAuth.h"
#import "XMPPPresence.h"
#import "XMPPSCRAMAuth.h"
#import "XMPPStanza.h"
#import "XMPPXMLElementBuilder.h"

#import "namespaces.h"

#import <ObjFW/macros.h>

@interface XMPPConnection () <OFDNSResolverDelegate, OFTCPSocketDelegate,
    OFXMLParserDelegate, OFXMLElementBuilderDelegate>
- (void)xmpp_tryNextSRVRecord;
-  (bool)xmpp_parseBuffer: (const void *)buffer
		   length: (size_t)length;
- (void)xmpp_startStream;
- (void)xmpp_handleStanza: (OFXMLElement *)element;
- (void)xmpp_handleStream: (OFXMLElement *)element;
- (void)xmpp_handleTLS: (OFXMLElement *)element;
- (void)xmpp_handleSASL: (OFXMLElement *)element;
- (void)xmpp_handleIQ: (XMPPIQ *)IQ;
- (void)xmpp_handleMessage: (XMPPMessage *)message;
- (void)xmpp_handlePresence: (XMPPPresence *)presence;
- (void)xmpp_handleFeatures: (OFXMLElement *)element;
- (void)xmpp_sendAuth: (OFString *)authName;
- (void)xmpp_sendResourceBind;
- (void)xmpp_sendStreamError: (OFString *)condition
			text: (OFString *)text;
- (void)xmpp_handleResourceBindForConnection: (XMPPConnection *)connection
					  IQ: (XMPPIQ *)IQ;
- (void)xmpp_sendSession;
- (void)xmpp_handleSessionForConnection: (XMPPConnection *)connection
				     IQ: (XMPPIQ *)IQ;
- (OFString *)xmpp_IDNAToASCII: (OFString *)domain;
- (XMPPMulticastDelegate *)xmpp_delegates;
@end

@implementation XMPPConnection
@synthesize username = _username, resource = _resource, server = _server;
@synthesize domain = _domain, password = _password, JID = _JID, port = _port;
@synthesize usesAnonymousAuthentication = _usesAnonymousAuthentication;
@synthesize language = _language, privateKeyFile = _privateKeyFile;
@synthesize certificateFile = _certificateFile, socket = _socket;
@synthesize encryptionRequired = _encryptionRequired, encrypted = _encrypted;
@synthesize supportsRosterVersioning = _supportsRosterVersioning;
@synthesize supportsStreamManagement = _supportsStreamManagement;

+ (instancetype)connection
{
	return [[[self alloc] init] autorelease];
}

- (instancetype)init
{
	self = [super init];

	@try {
		_port = 5222;
		_delegates = [[XMPPMulticastDelegate alloc] init];
		_callbacks = [[OFMutableDictionary alloc] init];
	} @catch (id e) {
		[self release];
		@throw e;
	}

	return self;
}

- (void)dealloc
{
	[_socket release];
	[_parser release];
	[_elementBuilder release];
	[_username release];
	[_password release];
	[_privateKeyFile release];
	[_certificateFile release];
	[_server release];
	[_domain release];
	[_resource release];
	[_JID release];
	[_nextSRVRecords release];
	[_delegates release];
	[_callbacks release];
	[_authModule release];

	[super dealloc];
}

- (void)setUsername: (OFString *)username
{
	OFString *old = _username;

	if (username != nil) {
		char *node;
		Stringprep_rc rc;

		if ((rc = stringprep_profile(username.UTF8String, &node,
		    "SASLprep", 0)) != STRINGPREP_OK)
			@throw [XMPPStringPrepFailedException
			    exceptionWithConnection: self
					    profile: @"SASLprep"
					     string: username];

		@try {
			_username = [[OFString alloc] initWithUTF8String: node];
		} @finally {
			free(node);
		}
	} else
		_username = nil;

	[old release];
}

- (void)setResource: (OFString *)resource
{
	OFString *old = _resource;

	if (resource != nil) {
		char *res;
		Stringprep_rc rc;

		if ((rc = stringprep_profile(resource.UTF8String, &res,
		    "Resourceprep", 0)) != STRINGPREP_OK)
			@throw [XMPPStringPrepFailedException
			    exceptionWithConnection: self
					    profile: @"Resourceprep"
					     string: resource];

		@try {
			_resource = [[OFString alloc] initWithUTF8String: res];
		} @finally {
			free(res);
		}
	} else
		_resource = nil;

	[old release];
}

- (void)setServer: (OFString *)server
{
	OFString *old = _server;

	if (server != nil)
		_server = [self xmpp_IDNAToASCII: server];
	else
		_server = nil;

	[old release];
}

- (void)setDomain: (OFString *)domain
{
	OFString *oldDomain = _domain;
	OFString *oldDomainToASCII = _domainToASCII;

	if (domain != nil) {
		char *srv;
		Stringprep_rc rc;

		if ((rc = stringprep_profile(domain.UTF8String, &srv,
		    "Nameprep", 0)) != STRINGPREP_OK)
			@throw [XMPPStringPrepFailedException
			    exceptionWithConnection: self
					    profile: @"Nameprep"
					     string: domain];

		@try {
			_domain = [[OFString alloc] initWithUTF8String: srv];
		} @finally {
			free(srv);
		}

		_domainToASCII = [self xmpp_IDNAToASCII: _domain];
	} else {
		_domain = nil;
		_domainToASCII = nil;
	}

	[oldDomain release];
	[oldDomainToASCII release];
}

- (void)setPassword: (OFString *)password
{
	OFString *old = _password;

	if (password != nil) {
		char *pass;
		Stringprep_rc rc;

		if ((rc = stringprep_profile(password.UTF8String, &pass,
		    "SASLprep", 0)) != STRINGPREP_OK)
			@throw [XMPPStringPrepFailedException
			    exceptionWithConnection: self
					    profile: @"SASLprep"
					     string: password];

		@try {
			_password = [[OFString alloc] initWithUTF8String: pass];
		} @finally {
			free(pass);
		}
	} else
		_password = nil;

	[old release];
}

-     (void)socket: (OFTCPSocket *)sock
  didConnectToHost: (OFString *)host
	      port: (uint16_t)port
	 exception: (id)exception
{
	if (exception != nil) {
		if (_nextSRVRecords.count > 0) {
			[self xmpp_tryNextSRVRecord];
			return;
		}

		[_delegates broadcastSelector: @selector(connection:
						   didThrowException:)
				   withObject: self
				   withObject: exception];
		return;
	}

	[self xmpp_startStream];

	[_socket asyncReadIntoBuffer: _buffer
			      length: XMPP_CONNECTION_BUFFER_LENGTH];
}

- (void)xmpp_tryNextSRVRecord
{
	OFSRVDNSResourceRecord *record =
	    [[[_nextSRVRecords objectAtIndex: 0] copy] autorelease];

	if (_nextSRVRecords.count == 0) {
		[_nextSRVRecords release];
		_nextSRVRecords = nil;
	}

	[_socket asyncConnectToHost: record.target
			       port: record.port];
}

-	(void)resolver: (OFDNSResolver *)resolver
  didResolveDomainName: (OFString *)domainName
	 answerRecords: (OFDictionary *)answerRecords
      authorityRecords: (OFDictionary *)authorityRecords
     additionalRecords: (OFDictionary *)additionalRecords
	     exception: (id)exception
{
	OFMutableArray *records = [OFMutableArray array];

	if (exception != nil) {
		[_delegates
		    broadcastSelector: @selector(connection:didThrowException:)
			   withObject: self
			   withObject: exception];
		return;
	}

	for (OFDNSResourceRecord *record in
	    [answerRecords objectForKey: domainName])
		if ([record isKindOfClass: [OFSRVDNSResourceRecord class]])
		       [records addObject: record];

	/* TODO: Sort records */
	[records makeImmutable];

	if (records.count == 0) {
		/* Fall back to A / AAAA record. */
		[_socket asyncConnectToHost: _domainToASCII
				       port: _port];
		return;
	}

	[_nextSRVRecords release];
	_nextSRVRecords = nil;
	_nextSRVRecords = [records mutableCopy];
	[self xmpp_tryNextSRVRecord];
}

- (void)asyncConnect
{
	void *pool = objc_autoreleasePoolPush();

	if (_socket != nil)
		@throw [OFAlreadyConnectedException exception];

	_socket = [[OFTCPSocket alloc] init];
	[_socket setDelegate: self];

	if (_server != nil)
		[_socket asyncConnectToHost: _server
				       port: _port];
	else {
		OFString *SRVDomain = [_domainToASCII
		    stringByPrependingString: @"_xmpp-client._tcp."];
		[[OFThread DNSResolver]
		    asyncResolveHost: SRVDomain
			 recordClass: OF_DNS_RESOURCE_RECORD_CLASS_IN
			  recordType: OF_DNS_RESOURCE_RECORD_TYPE_SRV
			    delegate: self];
	}

	objc_autoreleasePoolPop(pool);
}

-  (bool)xmpp_parseBuffer: (const void *)buffer
		   length: (size_t)length
{
	if (_socket.atEndOfStream) {
		[_delegates broadcastSelector: @selector(connectionWasClosed:
						   error:)
				   withObject: self
				   withObject: nil];
		return false;
	}

	@try {
		[_parser parseBuffer: buffer
			     length: length];
	} @catch (OFMalformedXMLException *e) {
		[self xmpp_sendStreamError: @"bad-format"
				      text: nil];
		[self close];
		return false;
	}

	return true;
}

- (void)parseBuffer: (const void *)buffer
	     length: (size_t)length
{
	[self xmpp_parseBuffer: buffer
			length: length];

	[_oldParser release];
	[_oldElementBuilder release];

	_oldParser = nil;
	_oldElementBuilder = nil;
}

-      (bool)stream: (OF_KINDOF(OFStream *))stream
  didReadIntoBuffer: (void *)buffer
	     length: (size_t)length
	  exception: (id)exception
{
	if (exception != nil) {
		[_delegates broadcastSelector: @selector(connection:
						   didThrowException:)
				   withObject: self
				   withObject: exception];
		[self close];
		return false;
	}

	@try {
		if (![self xmpp_parseBuffer: buffer
				     length: length])
			return false;
	} @catch (id e) {
		[_delegates broadcastSelector: @selector(connection:
						   didThrowException:)
				   withObject: self
				   withObject: e];
		[self close];
		return false;
	}

	if (_oldParser != nil || _oldElementBuilder != nil) {
		[_oldParser release];
		[_oldElementBuilder release];

		_oldParser = nil;
		_oldElementBuilder = nil;

		[_socket asyncReadIntoBuffer: _buffer
				      length: XMPP_CONNECTION_BUFFER_LENGTH];
		return false;
	}

	return true;
}

- (bool)streamOpen
{
	return _streamOpen;
}

- (bool)checkCertificateAndGetReason: (OFString **)reason
{
	X509Certificate *cert;
	OFDictionary *SANs;
	bool serviceSpecific = false;
	SSLSocket *socket = (SSLSocket *)_socket;

	@try {
		[socket verifyPeerCertificate];
	} @catch (SSLInvalidCertificateException *e) {
		if (reason != NULL)
			*reason = e.reason;

		return false;
	}

	cert = socket.peerCertificate;
	SANs = cert.subjectAlternativeName;

	if ([[SANs objectForKey: @"otherName"]
	    objectForKey: OID_SRVName] != nil ||
	    [SANs objectForKey: @"dNSName"] != nil ||
	    [SANs objectForKey: @"uniformResourceIdentifier"] != nil)
		serviceSpecific = true;

	if ([cert hasSRVNameMatchingDomain: _domainToASCII
				   service: @"xmpp-client"] ||
	    [cert hasDNSNameMatchingDomain: _domainToASCII])
		return true;

	if (!serviceSpecific &&
	    [cert hasCommonNameMatchingDomain: _domainToASCII])
		return true;

	return false;
}

- (void)sendStanza: (OFXMLElement *)element
{
	[_delegates broadcastSelector: @selector(connection:didSendElement:)
			   withObject: self
			   withObject: element];

	[_socket writeString: element.XMLString];
}

-   (void)sendIQ: (XMPPIQ *)IQ
  callbackTarget: (id)target
	selector: (SEL)selector
{
	void *pool = objc_autoreleasePoolPush();
	XMPPCallback *callback;
	OFString *ID, *key;

	if ((ID = IQ.ID) == nil) {
		ID = [self generateStanzaID];
		IQ.ID = ID;
	}

	if ((key = IQ.to.fullJID) == nil)
		key = _JID.bareJID;
	if (key == nil) // Only happens for resource bind
		key = @"bind";
	key = [key stringByAppendingString: ID];

	callback = [XMPPCallback callbackWithTarget: target
					   selector: selector];
	[_callbacks setObject: callback
		       forKey: key];

	objc_autoreleasePoolPop(pool);

	[self sendStanza: IQ];
}

#ifdef OF_HAVE_BLOCKS
-  (void)sendIQ: (XMPPIQ *)IQ
  callbackBlock: (xmpp_callback_block_t)block
{
	void *pool = objc_autoreleasePoolPush();
	XMPPCallback *callback;
	OFString *ID, *key;

	if ((ID = IQ.ID) == nil) {
		ID = [self generateStanzaID];
		IQ.ID = ID;
	}

	if ((key = IQ.to.fullJID) == nil)
		key = _JID.bareJID;
	if (key == nil) // Connection not yet bound, can't send stanzas
		@throw [OFInvalidArgumentException exception];
	key = [key stringByAppendingString: ID];

	callback = [XMPPCallback callbackWithBlock: block];
	[_callbacks setObject: callback
		       forKey: key];

	objc_autoreleasePoolPop(pool);

	[self sendStanza: IQ];
}
#endif

- (OFString *)generateStanzaID
{
	return [OFString stringWithFormat: @"objxmpp_%u", _lastID++];
}

-    (void)parser: (OFXMLParser *)parser
  didStartElement: (OFString *)name
	   prefix: (OFString *)prefix
	namespace: (OFString *)namespace
       attributes: (OFArray *)attributes
{
	if (![name isEqual: @"stream"]) {
		// No dedicated stream error for this, may not even be XMPP
		[self close];
		[_socket close];
		return;
	}

	if (![prefix isEqual: @"stream"]) {
		[self xmpp_sendStreamError: @"bad-namespace-prefix"
				      text: nil];
		return;
	}

	if (![namespace isEqual: XMPP_NS_STREAM]) {
		[self xmpp_sendStreamError: @"invalid-namespace"
				      text: nil];
		return;
	}

	for (OFXMLAttribute *attribute in attributes) {
		if ([attribute.name isEqual: @"from"] &&
		    ![attribute.stringValue isEqual: _domain]) {
			[self xmpp_sendStreamError: @"invalid-from"
					      text: nil];
			return;
		}
		if ([attribute.name isEqual: @"version"] &&
		    ![attribute.stringValue isEqual: @"1.0"]) {
			[self xmpp_sendStreamError: @"unsupported-version"
					      text: nil];
			return;
		}
	}

	parser.delegate = _elementBuilder;
}

- (void)elementBuilder: (OFXMLElementBuilder *)builder
       didBuildElement: (OFXMLElement *)element
{
	/* Ignore whitespace elements */
	if (element.name == nil)
		return;

	element.defaultNamespace = XMPP_NS_CLIENT;
	[element setPrefix: @"stream"
	      forNamespace: XMPP_NS_STREAM];

	[_delegates broadcastSelector: @selector(connection:didReceiveElement:)
			   withObject: self
			   withObject: element];

	if ([element.namespace isEqual: XMPP_NS_CLIENT])
		[self xmpp_handleStanza: element];

	if ([element.namespace isEqual: XMPP_NS_STREAM])
		[self xmpp_handleStream: element];

	if ([element.namespace isEqual: XMPP_NS_STARTTLS])
		[self xmpp_handleTLS: element];

	if ([element.namespace isEqual: XMPP_NS_SASL])
		[self xmpp_handleSASL: element];
}

- (void)elementBuilder: (OFXMLElementBuilder *)builder
  didNotExpectCloseTag: (OFString *)name
		prefix: (OFString *)prefix
	     namespace: (OFString *)ns
{
	if (![name isEqual: @"stream"] || ![prefix isEqual: @"stream"] ||
	    ![ns isEqual: XMPP_NS_STREAM])
		@throw [OFMalformedXMLException exception];
	else
		[self close];
}

- (void)xmpp_startStream
{
	OFString *langString = @"";

	/* Make sure we don't get any old events */
	_parser.delegate = nil;
	_elementBuilder.delegate = nil;

	/*
	 * We can't release them now, as we are currently inside them. Release
	 * them the next time the parser returns.
	 */
	_oldParser = _parser;
	_oldElementBuilder = _elementBuilder;

	_parser = [[OFXMLParser alloc] init];
	_parser.delegate = self;

	_elementBuilder = [[XMPPXMLElementBuilder alloc] init];
	_elementBuilder.delegate = self;

	if (_language != nil)
		langString = [OFString stringWithFormat: @"xml:lang='%@' ",
							 _language];

	[_socket writeFormat: @"<?xml version='1.0'?>\n"
			      @"<stream:stream to='%@' "
			      @"xmlns='" XMPP_NS_CLIENT @"' "
			      @"xmlns:stream='" XMPP_NS_STREAM @"' %@"
			      @"version='1.0'>", _domain, langString];

	_streamOpen = true;
}

- (void)close
{
	if (_streamOpen)
		[_socket writeString: @"</stream:stream>"];

	[_oldParser release];
	_oldParser = nil;
	[_oldElementBuilder release];
	_oldElementBuilder = nil;
	[_authModule release];
	_authModule = nil;
	[_socket release];
	_socket = nil;
	[_JID release];
	_JID = nil;
	_streamOpen = _needsSession = _encrypted = false;
	_supportsRosterVersioning = _supportsStreamManagement = false;
	_lastID = 0;
}

- (void)xmpp_handleStanza: (OFXMLElement *)element
{
	if ([element.name isEqual: @"iq"]) {
		[self xmpp_handleIQ: [XMPPIQ stanzaWithElement: element]];
		return;
	}

	if ([element.name isEqual: @"message"]) {
		[self xmpp_handleMessage:
		    [XMPPMessage stanzaWithElement: element]];
		return;
	}

	if ([element.name isEqual: @"presence"]) {
		[self xmpp_handlePresence:
		    [XMPPPresence stanzaWithElement: element]];
		return;
	}

	[self xmpp_sendStreamError: @"unsupported-stanza-type"
			      text: nil];
}


- (void)xmpp_handleStream: (OFXMLElement *)element
{
	if ([element.name isEqual: @"features"]) {
		[self xmpp_handleFeatures: element];
		return;
	}

	if ([element.name isEqual: @"error"]) {
		OFString *condition, *reason;
		[self close];

		[_delegates broadcastSelector: @selector(connectionWasClosed:)
				   withObject: self
				   withObject: element];

		condition = [[element elementsForNamespace:
		    XMPP_NS_XMPP_STREAM].firstObject name];

		if (condition == nil)
			condition = @"undefined";

		reason = [element
		    elementForName: @"text"
			 namespace: XMPP_NS_XMPP_STREAM].stringValue;

		@throw [XMPPStreamErrorException
		    exceptionWithConnection: self
				  condition: condition
				     reason: reason];
		return;
	}

	assert(0);
}

- (void)xmpp_handleTLS: (OFXMLElement *)element
{
	if ([element.name isEqual: @"proceed"]) {
		/* FIXME: Catch errors here */
		SSLSocket *newSock;

		[_delegates broadcastSelector: @selector(
						   connectionWillUpgradeToTLS:)
				   withObject: self];

		newSock = [[SSLSocket alloc] initWithSocket: _socket];
		newSock.certificateVerificationEnabled = false;
#if 0
		/* FIXME: Not yet implemented by ObjOpenSSL */
		[newSock setCertificateFile: _certificateFile];
		[newSock setPrivateKeyFile: _privateKeyFile];
		[newSock setPrivateKeyPassphrase: _privateKeyPassphrase];
#endif
		[newSock startTLSWithExpectedHost: nil];
		[_socket release];
		_socket = newSock;
		[_socket setDelegate: self];

		_encrypted = true;

		[_delegates broadcastSelector: @selector(
						   connectionDidUpgradeToTLS:)
				   withObject: self];

		/* Stream restart */
		[self xmpp_startStream];

		return;
	}

	if ([element.name isEqual: @"failure"])
		/* TODO: Find/create an exception to throw here */
		@throw [OFException exception];

	assert(0);
}

- (void)xmpp_handleSASL: (OFXMLElement *)element
{
	if ([element.name isEqual: @"challenge"]) {
		OFXMLElement *responseTag;
		OFData *challenge =
		    [OFData dataWithBase64EncodedString: element.stringValue];
		OFData *response = [_authModule continueWithData: challenge];

		responseTag = [OFXMLElement elementWithName: @"response"
						  namespace: XMPP_NS_SASL];
		if (response) {
			if (response.count == 0)
				responseTag.stringValue = @"=";
			else
				responseTag.stringValue =
				    response.stringByBase64Encoding;
		}

		[self sendStanza: responseTag];
		return;
	}

	if ([element.name isEqual: @"success"]) {
		[_authModule continueWithData: [OFData
		    dataWithBase64EncodedString: element.stringValue]];

		[_delegates broadcastSelector: @selector(
						   connectionWasAuthenticated:)
				   withObject: self];

		/* Stream restart */
		[self xmpp_startStream];

		return;
	}

	if ([element.name isEqual: @"failure"]) {
		/* FIXME: Do more parsing/handling */
		@throw [XMPPAuthFailedException
		    exceptionWithConnection: self
				     reason: element.XMLString];
	}

	assert(0);
}

- (void)xmpp_handleIQ: (XMPPIQ *)IQ
{
	bool handled = false;
	XMPPCallback *callback;
	OFString *key;

	if ((key = IQ.from.fullJID) == nil)
		key = _JID.bareJID;
	if (key == nil) // Only happens for resource bind
		key = @"bind";
	key = [key stringByAppendingString: IQ.ID];

	if ((callback = [_callbacks objectForKey: key])) {
		[callback runWithIQ: IQ
			 connection: self];
		[_callbacks removeObjectForKey: key];
		return;
	}

	handled = [_delegates broadcastSelector: @selector(
						     connection:didReceiveIQ:)
				     withObject: self
				     withObject: IQ];

	if (!handled && ![IQ.type isEqual: @"error"] &&
	    ![IQ.type isEqual: @"result"]) {
		[self sendStanza: [IQ errorIQWithType: @"cancel"
					    condition: @"service-unavailable"]];
	}
}

- (void)xmpp_handleMessage: (XMPPMessage *)message
{
	[_delegates broadcastSelector: @selector(connection:didReceiveMessage:)
			   withObject: self
			   withObject: message];
}

- (void)xmpp_handlePresence: (XMPPPresence *)presence
{
	[_delegates broadcastSelector: @selector(connection:didReceivePresence:)
			   withObject: self
			   withObject: presence];
}

- (void)xmpp_handleFeatures: (OFXMLElement *)element
{
	OFXMLElement *startTLS = [element elementForName: @"starttls"
					       namespace: XMPP_NS_STARTTLS];
	OFXMLElement *bind = [element elementForName: @"bind"
					   namespace: XMPP_NS_BIND];
	OFXMLElement *session = [element elementForName: @"session"
					      namespace: XMPP_NS_SESSION];
	OFXMLElement *mechs = [element elementForName: @"mechanisms"
					    namespace: XMPP_NS_SASL];
	OFMutableSet *mechanisms = [OFMutableSet set];

	if (!_encrypted && startTLS != nil) {
		[self sendStanza:
		    [OFXMLElement elementWithName: @"starttls"
					namespace: XMPP_NS_STARTTLS]];
		return;
	}

	if (_encryptionRequired && !_encrypted)
		/* TODO: Find/create an exception to throw here */
		@throw [OFException exception];

	if ([element elementForName: @"ver"
			  namespace: XMPP_NS_ROSTERVER] != nil)
		_supportsRosterVersioning = true;

	if ([element elementForName: @"sm"
			  namespace: XMPP_NS_SM] != nil)
		_supportsStreamManagement = true;

	if (mechs != nil) {
		for (OFXMLElement *mech in mechs.children)
			[mechanisms addObject: mech.stringValue];

		if (_usesAnonymousAuthentication) {
			if (![mechanisms containsObject: @"ANONYMOUS"])
				@throw [XMPPAuthFailedException
				    exceptionWithConnection: self
						     reason: @"No supported "
							     @"auth mechanism"];

			_authModule = [[XMPPANONYMOUSAuth alloc] init];
			[self xmpp_sendAuth: @"ANONYMOUS"];
			return;
		}

		if (_privateKeyFile != nil && _certificateFile != nil &&
		    [mechanisms containsObject: @"EXTERNAL"]) {
			_authModule = [[XMPPEXTERNALAuth alloc] init];
			[self xmpp_sendAuth: @"EXTERNAL"];
			return;
		}

		if ([mechanisms containsObject: @"SCRAM-SHA-1-PLUS"]) {
			_authModule = [[XMPPSCRAMAuth alloc]
			    initWithAuthcid: _username
				   password: _password
				 connection: self
				       hash: [OFSHA1Hash class]
			      plusAvailable: true];
			[self xmpp_sendAuth: @"SCRAM-SHA-1-PLUS"];
			return;
		}

		if ([mechanisms containsObject: @"SCRAM-SHA-1"]) {
			_authModule = [[XMPPSCRAMAuth alloc]
			    initWithAuthcid: _username
				   password: _password
				 connection: self
				       hash: [OFSHA1Hash class]
			      plusAvailable: false];
			[self xmpp_sendAuth: @"SCRAM-SHA-1"];
			return;
		}

		if ([mechanisms containsObject: @"PLAIN"] && _encrypted) {
			_authModule = [[XMPPPLAINAuth alloc]
			    initWithAuthcid: _username
				   password: _password];
			[self xmpp_sendAuth: @"PLAIN"];
			return;
		}

		@throw [XMPPAuthFailedException
		    exceptionWithConnection: self
				     reason: @"No supported auth mechanism"];

	}

	if (session != nil && [session elementForName: @"optional"
					    namespace: XMPP_NS_SESSION] == nil)
		_needsSession = true;

	if (bind != nil) {
		[self xmpp_sendResourceBind];
		return;
	}

	assert(0);
}

- (void)xmpp_sendAuth: (OFString *)authName
{
	OFXMLElement *authTag;
	OFData *initialMessage = [_authModule initialMessage];

	authTag = [OFXMLElement elementWithName: @"auth"
				      namespace: XMPP_NS_SASL];
	[authTag addAttributeWithName: @"mechanism"
			  stringValue: authName];
	if (initialMessage != nil) {
		if (initialMessage.count == 0)
			authTag.stringValue = @"=";
		else
			authTag.stringValue =
			    initialMessage.stringByBase64Encoding;
	}

	[self sendStanza: authTag];
}

- (void)xmpp_sendResourceBind
{
	XMPPIQ *IQ;
	OFXMLElement *bind;

	IQ = [XMPPIQ IQWithType: @"set"
			     ID: [self generateStanzaID]];

	bind = [OFXMLElement elementWithName: @"bind"
				   namespace: XMPP_NS_BIND];

	if (_resource != nil)
		[bind addChild: [OFXMLElement elementWithName: @"resource"
						    namespace: XMPP_NS_BIND
						  stringValue: _resource]];

	[IQ addChild: bind];

	[self	    sendIQ: IQ
	    callbackTarget: self
		  selector: @selector(xmpp_handleResourceBindForConnection:
				IQ:)];
}

- (void)xmpp_sendStreamError: (OFString *)condition
			text: (OFString *)text
{
	OFXMLElement *error = [OFXMLElement
	    elementWithName: @"error"
		  namespace: XMPP_NS_STREAM];
	[error setPrefix: @"stream"
	    forNamespace: XMPP_NS_STREAM];
	[error addChild: [OFXMLElement elementWithName: condition
					     namespace: XMPP_NS_XMPP_STREAM]];
	if (text)
		[error	   addChild: [OFXMLElement
		    elementWithName: @"text"
			  namespace: XMPP_NS_XMPP_STREAM
			stringValue: text]];
	_parser.delegate = nil;
	[self sendStanza: error];
	[self close];
}

- (void)xmpp_handleResourceBindForConnection: (XMPPConnection *)connection
					  IQ: (XMPPIQ *)IQ
{
	OFXMLElement *bindElement, *JIDElement;

	assert([IQ.type isEqual: @"result"]);

	bindElement = [IQ elementForName: @"bind"
			       namespace: XMPP_NS_BIND];

	assert(bindElement != nil);

	JIDElement = [bindElement elementForName: @"jid"
				       namespace: XMPP_NS_BIND];
	_JID = [[XMPPJID alloc] initWithString: JIDElement.stringValue];

	if (_needsSession) {
		[self xmpp_sendSession];
		return;
	}

	[_delegates broadcastSelector: @selector(connection:wasBoundToJID:)
			   withObject: self
			   withObject: _JID];
}

- (void)xmpp_sendSession
{
	XMPPIQ *IQ = [XMPPIQ IQWithType: @"set"
				     ID: [self generateStanzaID]];

	[IQ addChild: [OFXMLElement elementWithName: @"session"
					  namespace: XMPP_NS_SESSION]];

	[self	    sendIQ: IQ
	    callbackTarget: self
		  selector: @selector(xmpp_handleSessionForConnection:IQ:)];
}

- (void)xmpp_handleSessionForConnection: (XMPPConnection *)connection
				     IQ: (XMPPIQ *)IQ
{
	if (![IQ.type isEqual: @"result"])
		OF_ENSURE(0);

	[_delegates broadcastSelector: @selector(connection:wasBoundToJID:)
			   withObject: self
			   withObject: _JID];
}

- (OFString *)xmpp_IDNAToASCII: (OFString *)domain
{
	OFString *ret;
	char *cDomain;
	Idna_rc rc;

	if ((rc = idna_to_ascii_8z(domain.UTF8String,
	    &cDomain, IDNA_USE_STD3_ASCII_RULES)) != IDNA_SUCCESS)
		@throw [XMPPIDNATranslationFailedException
		    exceptionWithConnection: self
				  operation: @"ToASCII"
				     string: domain];

	@try {
		ret = [[OFString alloc] initWithUTF8String: cDomain];
	} @finally {
		free(cDomain);
	}

	return ret;
}

- (void)setDataStorage: (id <XMPPStorage>)dataStorage
{
	if (_streamOpen)
		/* FIXME: Find a better exception! */
		@throw [OFInvalidArgumentException exception];

	_dataStorage = dataStorage;
}

- (id <XMPPStorage>)dataStorage
{
	return _dataStorage;
}

- (void)addDelegate: (id <XMPPConnectionDelegate>)delegate
{
	[_delegates addDelegate: delegate];
}

- (void)removeDelegate: (id <XMPPConnectionDelegate>)delegate
{
	[_delegates removeDelegate: delegate];
}

- (XMPPMulticastDelegate *)xmpp_delegates
{
	return _delegates;
}
@end