/*
* 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 () <OFDNSResolverQueryDelegate, 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
didPerformQuery: (OFString *)domainName
response: (OFDNSResponse *)response
exception: (id)exception
{
OFMutableArray *records = [OFMutableArray array];
if (exception != nil)
response = nil;
for (OFDNSResourceRecord *record in
[response.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."];
OFDNSQuery *query = [OFDNSQuery
queryWithDomainName: SRVDomain
DNSClass: OF_DNS_CLASS_IN
recordType: OF_DNS_RECORD_TYPE_SRV];
[[OFThread DNSResolver] asyncPerformQuery: query
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