diff --git a/.travis.yml b/.travis.yml index 963532738b..a371c394fa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ before_install: - export PATH="$(pwd)/gradle-${GRADLE_VERSION}/bin:$PATH" install: gradle assemble --stacktrace -script: gradle check +script: gradle check --stacktrace after_success: - JAVAC_VERSION=$((javac -version) 2>&1) diff --git a/documentation/dnssec.md b/documentation/dnssec.md new file mode 100644 index 0000000000..f16c4b3828 --- /dev/null +++ b/documentation/dnssec.md @@ -0,0 +1,75 @@ +DNSSEC and DANE +=============== + +[Back](index.md) + +**DNSSEC and DANE support in Smack and MiniDNS is still in its +infancy. It should be considered experimental and not ready for +production use at this time.** We would like to see more thorough +testing and review by the security community. If you can help, then +please do not hesitate to contact us. + +About +----- + +DNSSEC ([RFC 4033](https://tools.ietf.org/html/rfc4033) and others) +authenticates DNS answers, positive and negative ones. This means that +if a DNS response secured by DNSSEC turns out to be authentic, then +you can be sure that the domain either exists, and that the returned +resource records (RRs) are the ones the domain owner authorized, or +that the domain does not exists and that nobody tried to fake its non +existence. + +The tricky part is that an application using DNSSEC can not determine +whether a domain uses DNSSEC, does not use DNSSEC or if someone +downgraded your DNS query using DNSSEC to a response without DNSSEC. + +[DANE](https://tools.ietf.org/html/rfc6698) allows the verification of +a TLS certificate with information stored in the DNS system and +secured by DNSSEC. Thus DANE requires DNSSEC. + +Prerequisites +------------- + +From the three DNS resolver providers (MiniDNS, javax, dnsjava) +supported by Smack only [MiniDNS](https://github.com/rtreffer/minidns) +currently supports DNSSEC. MiniDNS is the default resolver when +smack-android is used. For other configurations, make sure to add +smack-resolver-minidns to your dependencies and call +`MiniDnsResolver.setup()` prior using Smack (e.g. in a `static {}` +code block). + +DNSSEC API +---------- + +Smack's DNSSEC API is very simple: Just use +`ConnectionConfiguration.Builder..setDnssecMode(DnssecMode)` to enable +DNSSEC. `DnssecMode` can be one of + +- `disabled` +- `needsDnssec` +- `needsDnssecAndDane` + +The default is `disabled`. + +If `needsDnssec` is used, then Smack will only connect if the DNS +results required to determine a host for the XMPP domain could be +verified using DNSSEC. + +If `needsDnssecAndDane` then DANE will be used to verify the XMPP +service's TLS certificate if STARTTLS is used. Note that you may want +to configure +`ConnectionConfiguration.Builder.setSecurityMode(SecurityMode.required)` +if you use this DNSSEC mode setting. + +Best practices +-------------- + +We recommend that applications using Smack's DNSSEC API do not ask the +user if DNSSEC is avaialble. Instead they should check for DNSSEC +suport on every connection attempt. Once DNSSEC support has been +discovered, the application should use the `needsDnssec` mode for all +future connection attempts. The same scheme can be applied when using +DANE. This approach is similar to the scheme established by +to +["HTTP Strict Transport Security" (HSTS, RFC 6797)](https://tools.ietf.org/html/rfc6797). diff --git a/documentation/extensions/blockingcommand.md b/documentation/extensions/blockingcommand.md new file mode 100644 index 0000000000..3e7e369ceb --- /dev/null +++ b/documentation/extensions/blockingcommand.md @@ -0,0 +1,63 @@ +Blocking Command +================ + +Allows to manage communications blocking. + + * Check push notifications support + * Get blocking list + * Block contact + * Unblock contact + * Unblock all + + +**XEP related:** [XEP-0191](http://xmpp.org/extensions/xep-0191.html) + + +Get an instance of Blocking Command Manager +------------------------------------------- + +``` +BlockingCommandManager blockingCommandManager = BlockingCommandManager.getInstanceFor(connection); +``` + + +Check blocking command support +------------------------------ + +``` +boolean isSupported = blockingCommandManager.isSupportedByServer(); +``` + + +Get block list +-------------- + +``` +List blockList = blockingCommandManager.getBlockList(); +``` + + +Block contact +------------- + +``` +blockingCommandManager.blockContact(jid); +``` +*jid* is a `Jid` + + +Unblock contact +--------------- + +``` +blockingCommandManager.unblockContact(jid); +``` +*jid* is a `Jid` + + +Unblock all +----------- + +``` +blockingCommandManager.unblockAll(); +``` diff --git a/documentation/extensions/index.md b/documentation/extensions/index.md index 771afac33d..8d144e6564 100644 --- a/documentation/extensions/index.md +++ b/documentation/extensions/index.md @@ -63,6 +63,7 @@ Smack Extensions and currently supported XEPs of smack-extensions | Data Forms Layout | [XEP-0141](http://xmpp.org/extensions/xep-0141.html) | Enables an application to specify form layouts. | | Personal Eventing Protocol | [XEP-0163](http://xmpp.org/extensions/xep-0163.html) | Using the XMPP publish-subscribe protocol to broadcast state change events associated with an XMPP account. | | Message Delivery Receipts | [XEP-0184](http://xmpp.org/extensions/xep-0184.html) | Extension for message delivery receipts. The sender can request notification that the message has been delivered. | +| [Blocking Command](blockingcommand.md) | [XEP-0191](http://xmpp.org/extensions/xep-0191.html) | Communications blocking that is intended to be simpler than privacy lists (XEP-0016). | | XMPP Ping | [XEP-0199](http://xmpp.org/extensions/xep-0199.html) | Sending application-level pings over XML streams. | Entity Time | [XEP-0202](http://xmpp.org/extensions/xep-0202.html) | Allows entities to communicate their local time | | Delayed Delivery | [XEP-0203](http://xmpp.org/extensions/xep-0203.html) | Extension for communicating the fact that an XML stanza has been delivered with a delay. | @@ -78,16 +79,16 @@ Experimental Smack Extensions and currently supported XEPs of smack-experimental | Name | XEP | Description | |---------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------| -| Message Carbons | [XEP-0280](http://xmpp.org/extensions/xep-0280.html) | Keep all IM clients for a user engaged in a conversation, by carbon-copy outbound messages to all interested resources. +| Message Carbons | [XEP-0280](http://xmpp.org/extensions/xep-0280.html) | Keep all IM clients for a user engaged in a conversation, by carbon-copy outbound messages to all interested resources. | +| [Message Archive Management](mam.md) | [XEP-0313](http://xmpp.org/extensions/xep-0313.html) | Query and control an archive of messages stored on a server. | | [Internet of Things - Sensor Data](iot.md) | [XEP-0323](http://xmpp.org/extensions/xep-0323.html) | Sensor data interchange over XMPP. | | [Internet of Things - Provisioning](iot.md) | [XEP-0324](http://xmpp.org/extensions/xep-0324.html) | Provisioning, access rights and user priviliges for the Internet of Things. | | [Internet of Things - Control](iot.md) | [XEP-0325](http://xmpp.org/extensions/xep-0325.html) | Describes how to control devices or actuators in an XMPP-based sensor netowrk. | | [HTTP over XMPP transport](hoxt.md) | [XEP-0332](http://xmpp.org/extensions/xep-0332.html) | Allows to transport HTTP communication over XMPP peer-to-peer networks. | | JSON Containers | [XEP-0335](http://xmpp.org/extensions/xep-0335.html) | Encapsulation of JSON data within XMPP Stanzas. | | [Internet of Things - Discovery](iot.md) | [XEP-0347](http://xmpp.org/extensions/xep-0347.html) | Describes how Things can be installed and discovered by their owners. | -| Google GCM JSON payload | n/a | Semantically the same as XEP-0335: JSON Containers | | Client State Indication | [XEP-0352](http://xmpp.org/extensions/xep-0352.html) | A way for the client to indicate its active/inactive state. | -| [Message Archive Management](mam.md) | [XEP-0313](http://xmpp.org/extensions/xep-0313.html) | Query and control an archive of messages stored on a server. | +| Google GCM JSON payload | n/a | Semantically the same as XEP-0335: JSON Containers | Legacy Smack Extensions and currently supported XEPs of smack-legacy @@ -98,5 +99,4 @@ If a XEP becomes 'Deprecated' or 'Obsolete' the code will be moved to the *smack | Name | XEP | Description | |---------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------| | [Message Events](messageevents.md) | [XEP-0022](http://xmpp.org/extensions/xep-0022.html) | Requests and responds to message events. | - | [Roster Item Exchange](rosterexchange.md) | [XEP-0093](http://xmpp.org/extensions/xep-0093.html) | Allows roster data to be shared between users. | diff --git a/documentation/index.md b/documentation/index.md index 00e98e92e9..034f180709 100644 --- a/documentation/index.md +++ b/documentation/index.md @@ -9,6 +9,7 @@ * [Roster and Presence](roster.md) * [Processing Incoming Stanzas](processing.md) * [Provider Architecture](providers.md) + * [DNSSEC and DANE](dnssec.md) * [Debugging with Smack](debugging.md) * [Smack Extensions Manual](extensions/index.md) diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java index 295f360a61..d5281122cc 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -648,11 +648,10 @@ protected List populateHostAddresses() { // N.B.: Important to use config.serviceName and not AbstractXMPPConnection.serviceName if (config.host != null) { hostAddresses = new ArrayList(1); - HostAddress hostAddress; - hostAddress = new HostAddress(config.host, config.port); + HostAddress hostAddress = DNSUtil.getDNSResolver().lookupHostAddress(config.host, failedAddresses, config.getDnssecMode()); hostAddresses.add(hostAddress); } else { - hostAddresses = DNSUtil.resolveXMPPServiceDomain(config.getXMPPServiceDomain().toString(), failedAddresses); + hostAddresses = DNSUtil.resolveXMPPServiceDomain(config.getXMPPServiceDomain().toString(), failedAddresses, config.getDnssecMode()); } // If we reach this, then hostAddresses *must not* be empty, i.e. there is at least one host added, either the // config.host one or the host representing the service name by DNSUtil diff --git a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java index e540eb154f..ff6207dc98 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java @@ -32,12 +32,14 @@ import org.jivesoftware.smack.util.StringUtils; import org.jxmpp.jid.DomainBareJid; import org.jxmpp.jid.EntityBareJid; +import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Resourcepart; import org.jxmpp.stringprep.XmppStringprepException; import javax.net.SocketFactory; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; +import javax.net.ssl.X509TrustManager; import javax.security.auth.callback.CallbackHandler; /** @@ -96,6 +98,10 @@ public abstract class ConnectionConfiguration { private final boolean legacySessionDisabled; private final SecurityMode securityMode; + private final DnssecMode dnssecMode; + + private final X509TrustManager customX509TrustManager; + /** * */ @@ -134,6 +140,10 @@ protected ConnectionConfiguration(Builder builder) { proxy = builder.proxy; socketFactory = builder.socketFactory; + dnssecMode = builder.dnssecMode; + + customX509TrustManager = builder.customX509TrustManager; + securityMode = builder.securityMode; keystoreType = builder.keystoreType; keystorePath = builder.keystorePath; @@ -150,6 +160,11 @@ protected ConnectionConfiguration(Builder builder) { // If the enabledSaslmechanisms are set, then they must not be empty assert(enabledSaslMechanisms != null ? !enabledSaslMechanisms.isEmpty() : true); + + if (dnssecMode != DnssecMode.disabled && customSSLContext != null) { + throw new IllegalStateException("You can not use a custom SSL context with DNSSEC enabled"); + } + } /** @@ -182,6 +197,14 @@ public SecurityMode getSecurityMode() { return securityMode; } + public DnssecMode getDnssecMode() { + return dnssecMode; + } + + public X509TrustManager getCustomX509TrustManager() { + return customX509TrustManager; + } + /** * Retuns the path to the keystore file. The key store file contains the * certificates that may be used to authenticate the client to the server, @@ -341,6 +364,37 @@ public static enum SecurityMode { disabled } + /** + * Determines the requested DNSSEC security mode. + * Note that Smack's support for DNSSEC/DANE is experimental! + *

+ * The default '{@link #disabled}' means that neither DNSSEC nor DANE verification will be performed. When + * '{@link #needsDnssec}' is used, then the connection will not be established if the resource records used to connect + * to the XMPP service are not authenticated by DNSSEC. Additionally, if '{@link #needsDnssecAndDane}' is used, then + * the XMPP service's TLS certificate is verified using DANE. + * + */ + public enum DnssecMode { + + /** + * Do not perform any DNSSEC authentication or DANE verification. + */ + disabled, + + /** + * Experimental! + * Require all DNS information to be authenticated by DNSSEC. + */ + needsDnssec, + + /** + * Experimental! + * Require all DNS information to be authenticated by DNSSEC and require the XMPP service's TLS certificate to be verified using DANE. + */ + needsDnssecAndDane, + + } + /** * Returns the username to use when trying to reconnect to the server. * @@ -436,6 +490,7 @@ public Set getEnabledSaslMechanisms() { */ public static abstract class Builder, C extends ConnectionConfiguration> { private SecurityMode securityMode = SecurityMode.ifpossible; + private DnssecMode dnssecMode = DnssecMode.disabled; private String keystorePath = System.getProperty("javax.net.ssl.keyStore"); private String keystoreType = "jks"; private String pkcs11Library = "pkcs11.config"; @@ -459,6 +514,7 @@ public static abstract class Builder, C extends Connecti private boolean allowEmptyOrNullUsername = false; private boolean saslMechanismsSealed; private Set enabledSaslMechanisms; + private X509TrustManager customX509TrustManager; protected Builder() { } @@ -481,7 +537,7 @@ public B setUsernameAndPassword(CharSequence username, String password) { } /** - * Set the service name of this XMPP service (i.e., the XMPP domain). + * Set the XMPP domain. The XMPP domain is what follows after the '@' sign in XMPP addresses (JIDs). * * @param serviceName the service name * @return a reference to this builder. @@ -493,13 +549,25 @@ public B setServiceName(DomainBareJid serviceName) { } /** - * Set the service name of this XMPP service (i.e., the XMPP domain). + * Set the XMPP domain. The XMPP domain is what follows after the '@' sign in XMPP addresses (JIDs). + * + * @param xmppDomain the XMPP domain. + * @return a reference to this builder. + */ + public B setXmppDomain(DomainBareJid xmppDomain) { + this.xmppServiceDomain = xmppDomain; + return getThis(); + } + + /** + * Set the XMPP domain. The XMPP domain is what follows after the '@' sign in XMPP addresses (JIDs). * - * @param xmppServiceDomain the service name + * @param xmppServiceDomain the XMPP domain. * @return a reference to this builder. + * @throws XmppStringprepException if the given string is not a domain bare JID. */ - public B setXmppDomain(DomainBareJid xmppServiceDomain) { - this.xmppServiceDomain = xmppServiceDomain; + public B setXmppDomain(String xmppServiceDomain) throws XmppStringprepException { + this.xmppServiceDomain = JidCreate.domainBareFrom(xmppServiceDomain); return getThis(); } @@ -556,6 +624,16 @@ public B setCallbackHandler(CallbackHandler callbackHandler) { return getThis(); } + public B setDnssecMode(DnssecMode dnssecMode) { + this.dnssecMode = Objects.requireNonNull(dnssecMode, "DNSSEC mode must not be null"); + return getThis(); + } + + public B setCustomX509TrustManager(X509TrustManager x509TrustManager) { + this.customX509TrustManager = x509TrustManager; + return getThis(); + } + /** * Sets the TLS security mode used when making the connection. By default, * the mode is {@link SecurityMode#ifpossible}. diff --git a/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java b/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java index 1889ea3bc5..311e8edb19 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/debugger/ConsoleDebugger.java @@ -37,7 +37,7 @@ * @author Gaston Dombiak */ public class ConsoleDebugger extends AbstractDebugger { - private final SimpleDateFormat dateFormatter = new SimpleDateFormat("hh:mm:ss aaa"); + private final SimpleDateFormat dateFormatter = new SimpleDateFormat("HH:mm:ss"); public ConsoleDebugger(XMPPConnection connection, Writer writer, Reader reader) { super(connection, writer, reader); diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java index 2a6321e1ef..6751e89a32 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Message.java @@ -114,6 +114,18 @@ public Message(String to, String body) throws XmppStringprepException { this(JidCreate.from(to), body); } + /** + * Creates a new message with the specified recipient and extension element. + * + * @param to + * @param extensionElement + * @since 4.2 + */ + public Message(Jid to, ExtensionElement extensionElement) { + this(to); + addExtension(extensionElement); + } + /** * Copy constructor. *

diff --git a/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java b/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java index cd9aedf779..976c60dd63 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/packet/Presence.java @@ -24,6 +24,7 @@ import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.TypedCloneable; import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jxmpp.jid.Jid; /** * Represents XMPP presence packets. Every presence stanza(/packet) has a type, which is one of @@ -78,6 +79,18 @@ public Presence(Type type) { setType(type); } + /** + * Creates a new presence with the given type and using the given XMPP address as recipient. + * + * @param to the recipient. + * @param type the type. + * @since 4.2 + */ + public Presence(Jid to, Type type) { + this(type); + setTo(to); + } + /** * Creates a new presence update with a specified status, priority, and mode. * diff --git a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java index 3a94612318..de9527ab13 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/sasl/SASLMechanism.java @@ -268,7 +268,7 @@ private void authenticateXOAUTH(String token) throws NotConnectedException, Inte * @throws InterruptedException */ public final void challengeReceived(String challengeString, boolean finalChallenge) throws SmackException, NotConnectedException, InterruptedException { - byte[] challenge = Base64.decode(challengeString); + byte[] challenge = Base64.decode((challengeString != null && challengeString.equals("=")) ? "" : challengeString); byte[] response = evaluateChallenge(challenge); if (finalChallenge) { return; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java index 54520b4c9c..391e030340 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java @@ -1,6 +1,6 @@ /** * - * Copyright 2003-2005 Jive Software. + * Copyright 2003-2005 Jive Software, 2016 Florian Schmaus. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,9 @@ import java.util.logging.Level; import java.util.logging.Logger; +import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.util.dns.DNSResolver; +import org.jivesoftware.smack.util.dns.SmackDaneProvider; import org.jivesoftware.smack.util.dns.HostAddress; import org.jivesoftware.smack.util.dns.SRVRecord; @@ -33,11 +35,13 @@ * Utility class to perform DNS lookups for XMPP services. * * @author Matt Tucker + * @author Florian Schmaus */ public class DNSUtil { private static final Logger LOGGER = Logger.getLogger(DNSUtil.class.getName()); private static DNSResolver dnsResolver = null; + private static SmackDaneProvider daneProvider; /** * International Domain Name transformer. @@ -62,7 +66,7 @@ public String transform(String string) { * @param resolver */ public static void setDNSResolver(DNSResolver resolver) { - dnsResolver = resolver; + dnsResolver = Objects.requireNonNull(resolver); } /** @@ -74,6 +78,23 @@ public static DNSResolver getDNSResolver() { return dnsResolver; } + /** + * Set the DANE provider that should be used when DANE is enabled. + * + * @param daneProvider + */ + public static void setDaneProvider(SmackDaneProvider daneProvider) { + daneProvider = Objects.requireNonNull(daneProvider); + } + + /** + * Returns the currently active DANE provider used when DANE is enabled. + * + * @return the active DANE provider + */ + public static SmackDaneProvider getDaneProvider() { + return daneProvider; + } /** * Set the IDNA (Internationalizing Domain Names in Applications, RFC 3490) transformer. @@ -109,15 +130,10 @@ private static enum DomainType { * @return List of HostAddress, which encompasses the hostname and port that the * XMPP server can be reached at for the specified domain. */ - public static List resolveXMPPServiceDomain(String domain, List failedAddresses) { + public static List resolveXMPPServiceDomain(String domain, List failedAddresses, DnssecMode dnssecMode) { domain = idnaTransformer.transform(domain); - if (dnsResolver == null) { - LOGGER.warning("No DNS Resolver active in Smack, will be unable to perform DNS SRV lookups"); - List addresses = new ArrayList(1); - addresses.add(new HostAddress(domain, 5222)); - return addresses; - } - return resolveDomain(domain, DomainType.Client, failedAddresses); + + return resolveDomain(domain, DomainType.Client, failedAddresses, dnssecMode); } /** @@ -134,25 +150,25 @@ public static List resolveXMPPServiceDomain(String domain, List resolveXMPPServerDomain(String domain, List failedAddresses) { + public static List resolveXMPPServerDomain(String domain, List failedAddresses, DnssecMode dnssecMode) { domain = idnaTransformer.transform(domain); - if (dnsResolver == null) { - LOGGER.warning("No DNS Resolver active in Smack, will be unable to perform DNS SRV lookups"); - List addresses = new ArrayList(1); - addresses.add(new HostAddress(domain, 5269)); - return addresses; - } - return resolveDomain(domain, DomainType.Server, failedAddresses); + + return resolveDomain(domain, DomainType.Server, failedAddresses, dnssecMode); } /** * * @param domain the domain. * @param domainType the XMPP domain type, server or client. - * @param failedAddresses on optional list that will be populated with host addresses that failed to resolve. + * @param failedAddresses a list that will be populated with host addresses that failed to resolve. * @return a list of resolver host addresses for this domain. */ - private static List resolveDomain(String domain, DomainType domainType, List failedAddresses) { + private static List resolveDomain(String domain, DomainType domainType, + List failedAddresses, DnssecMode dnssecMode) { + if (dnsResolver == null) { + throw new IllegalStateException("No DNS Resolver active in Smack"); + } + List addresses = new ArrayList(); // Step one: Do SRV lookups @@ -167,8 +183,9 @@ private static List resolveDomain(String domain, DomainType domainT default: throw new AssertionError(); } - try { - List srvRecords = dnsResolver.lookupSRVRecords(srvDomain); + + List srvRecords = dnsResolver.lookupSRVRecords(srvDomain, failedAddresses, dnssecMode); + if (srvRecords != null) { if (LOGGER.isLoggable(Level.FINE)) { String logMessage = "Resolved SRV RR for " + srvDomain + ":"; for (SRVRecord r : srvRecords) @@ -178,18 +195,12 @@ private static List resolveDomain(String domain, DomainType domainT List sortedRecords = sortSRVRecords(srvRecords); addresses.addAll(sortedRecords); } - catch (Exception e) { - LOGGER.log(Level.WARNING, "Exception while resovling SRV records for " + domain - + ". Consider adding '_xmpp-(server|client)._tcp' DNS SRV Records", e); - if (failedAddresses != null) { - HostAddress failedHostAddress = new HostAddress(srvDomain); - failedHostAddress.setException(e); - failedAddresses.add(failedHostAddress); - } - } // Step two: Add the hostname to the end of the list - addresses.add(new HostAddress(domain)); + HostAddress hostAddress = dnsResolver.lookupHostAddress(domain, failedAddresses, dnssecMode); + if (hostAddress != null) { + addresses.add(hostAddress); + } return addresses; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/TLSUtils.java b/smack-core/src/main/java/org/jivesoftware/smack/util/TLSUtils.java index 822543d1d2..6330640662 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/TLSUtils.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/TLSUtils.java @@ -118,7 +118,7 @@ public boolean verify(String hostname, SSLSession session) { * @param builder a connection configuration builder. * @return the given builder. */ - public static > B disableHostnameVerificationForTlsCertificicates(B builder) { + public static > B disableHostnameVerificationForTlsCertificates(B builder) { builder.setHostnameVerifier(DOES_NOT_VERIFY_VERIFIER); return builder; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/DNSResolver.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/DNSResolver.java index 89f1b404a9..052795bdad 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/DNSResolver.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/DNSResolver.java @@ -1,6 +1,6 @@ /** * - * Copyright 2013 Florian Schmaus + * Copyright 2013-2016 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,19 +16,67 @@ */ package org.jivesoftware.smack.util.dns; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; import java.util.List; +import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; + /** * Implementations of this interface define a class that is capable of resolving DNS addresses. * */ -public interface DNSResolver { +public abstract class DNSResolver { + + private final boolean supportsDnssec; + + protected DNSResolver(boolean supportsDnssec) { + this.supportsDnssec = supportsDnssec; + } /** * Gets a list of service records for the specified service. * @param name The symbolic name of the service. * @return The list of SRV records mapped to the service name. */ - List lookupSRVRecords(String name) throws Exception; + public final List lookupSRVRecords(String name, List failedAddresses, DnssecMode dnssecMode) { + checkIfDnssecRequestedAndSupported(dnssecMode); + return lookupSRVRecords0(name, failedAddresses, dnssecMode); + } + + protected abstract List lookupSRVRecords0(String name, List failedAddresses, DnssecMode dnssecMode); + + public final HostAddress lookupHostAddress(String name, List failedAddresses, DnssecMode dnssecMode) { + checkIfDnssecRequestedAndSupported(dnssecMode); + List inetAddresses = lookupHostAddress0(name, failedAddresses, dnssecMode); + if (inetAddresses == null) { + return null; + } + return new HostAddress(name, inetAddresses); + } + + protected List lookupHostAddress0(String name, List failedAddresses, DnssecMode dnssecMode) { + // Default implementation of a DNS name lookup for A/AAAA records. It is assumed that this method does never + // support DNSSEC. Subclasses are free to override this method. + if (dnssecMode != DnssecMode.disabled) { + throw new UnsupportedOperationException("This resolver does not support DNSSEC"); + } + + InetAddress[] inetAddressArray; + try { + inetAddressArray = InetAddress.getAllByName(name); + } catch (UnknownHostException e) { + failedAddresses.add(new HostAddress(name, e)); + return null; + } + + return Arrays.asList(inetAddressArray); + } + private final void checkIfDnssecRequestedAndSupported(DnssecMode dnssecMode) { + if (dnssecMode != DnssecMode.disabled && !supportsDnssec) { + throw new UnsupportedOperationException("This resolver does not support DNSSEC"); + } + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java index 3e93872f81..e9ae53717b 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java @@ -1,6 +1,6 @@ /** * - * Copyright © 2013-2014 Florian Schmaus + * Copyright © 2013-2016 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -30,6 +31,7 @@ public class HostAddress { private final String fqdn; private final int port; private final Map exceptions = new LinkedHashMap<>(); + private final List inetAddresses; /** * Creates a new HostAddress with the given FQDN. The port will be set to the default XMPP client port: 5222 @@ -37,9 +39,9 @@ public class HostAddress { * @param fqdn Fully qualified domain name. * @throws IllegalArgumentException If the fqdn is null. */ - public HostAddress(String fqdn) { + public HostAddress(String fqdn, List inetAddresses) { // Set port to the default port for XMPP client communication - this(fqdn, 5222); + this(fqdn, 5222, inetAddresses); } /** @@ -49,7 +51,7 @@ public HostAddress(String fqdn) { * @param port The port to connect on. * @throws IllegalArgumentException If the fqdn is null or port is out of valid range (0 - 65535). */ - public HostAddress(String fqdn, int port) { + public HostAddress(String fqdn, int port, List inetAddresses) { Objects.requireNonNull(fqdn, "FQDN is null"); if (port < 0 || port > 65535) throw new IllegalArgumentException( @@ -61,6 +63,24 @@ public HostAddress(String fqdn, int port) { this.fqdn = fqdn; } this.port = port; + if (inetAddresses.isEmpty()) { + throw new IllegalArgumentException("Must provide at least one InetAddress"); + } + this.inetAddresses = inetAddresses; + } + + /** + * Constructs a new failed HostAddress. This constructor is usually used when the DNS resolution of the domain name + * failed for some reason. + * + * @param fqdn the domain name of the host. + * @param e the exception causing the failure. + */ + public HostAddress(String fqdn, Exception e) { + this.fqdn = fqdn; + this.port = 5222; + inetAddresses = Collections.emptyList(); + setException(e); } public String getFQDN() { @@ -91,6 +111,10 @@ public Map getExceptions() { return Collections.unmodifiableMap(exceptions); } + public List getInetAddresses() { + return Collections.unmodifiableList(inetAddresses); + } + @Override public String toString() { return fqdn + ":" + port; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SRVRecord.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SRVRecord.java index 7da214c551..e84d846def 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SRVRecord.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SRVRecord.java @@ -1,6 +1,6 @@ /** * - * Copyright 2013-2014 Florian Schmaus + * Copyright 2013-2016 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,9 @@ */ package org.jivesoftware.smack.util.dns; +import java.net.InetAddress; +import java.util.List; + /** * A DNS SRV RR. * @@ -38,8 +41,8 @@ public class SRVRecord extends HostAddress implements Comparable { * @param weight Relative weight for records with same priority * @throws IllegalArgumentException fqdn is null or any other field is not in valid range (0-65535). */ - public SRVRecord(String fqdn, int port, int priority, int weight) { - super(fqdn, port); + public SRVRecord(String fqdn, int port, int priority, int weight, List inetAddresses) { + super(fqdn, port, inetAddresses); if (weight < 0 || weight > 65535) throw new IllegalArgumentException( "DNS SRV records weight must be a 16-bit unsiged integer (i.e. between 0-65535. Weight was: " diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneProvider.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneProvider.java new file mode 100644 index 0000000000..abb4f5da57 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneProvider.java @@ -0,0 +1,24 @@ +/** + * + * Copyright 2015-2016 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.dns; + +/** + * Implementations of this interface define a class that is capable of enabling DANE on a connection. + */ +public interface SmackDaneProvider { + SmackDaneVerifier newInstance(); +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneVerifier.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneVerifier.java new file mode 100644 index 0000000000..f50b387564 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneVerifier.java @@ -0,0 +1,34 @@ +/** + * + * Copyright 2015-2016 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.dns; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.X509TrustManager; +import java.security.KeyManagementException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; + +/** + * Implementations of this interface define a class that is capable of enabling DANE on a connection. + */ +public interface SmackDaneVerifier { + void init(SSLContext context, KeyManager[] km, X509TrustManager tm, SecureRandom random) throws KeyManagementException; + + void finish(SSLSocket socket) throws CertificateException; +} diff --git a/smack-core/src/test/java/org/jivesoftware/smack/SmackExceptionTest.java b/smack-core/src/test/java/org/jivesoftware/smack/SmackExceptionTest.java index 8f231049e5..c14f137407 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/SmackExceptionTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/SmackExceptionTest.java @@ -18,6 +18,9 @@ import static org.junit.Assert.assertEquals; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Collections; import java.util.LinkedList; import java.util.List; @@ -28,14 +31,20 @@ public class SmackExceptionTest { @Test - public void testConnectionException() { + public void testConnectionException() throws UnknownHostException { List failedAddresses = new LinkedList(); - HostAddress hostAddress = new HostAddress("foo.bar.example", 1234); + String host = "foo.bar.example"; + InetAddress inetAddress = InetAddress.getByAddress(host, new byte[] { 0, 0, 0, 0 }); + List inetAddresses = Collections.singletonList(inetAddress); + HostAddress hostAddress = new HostAddress(host, 1234, inetAddresses); hostAddress.setException(new Exception("Failed for some reason")); failedAddresses.add(hostAddress); - hostAddress = new HostAddress("barz.example", 5678); + host = "barz.example"; + inetAddress = InetAddress.getByAddress(host, new byte[] { 0, 0, 0, 0 }); + inetAddresses = Collections.singletonList(inetAddress); + hostAddress = new HostAddress(host, 5678, inetAddresses); hostAddress.setException(new Exception("Failed for some other reason")); failedAddresses.add(hostAddress); diff --git a/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebugger.java b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebugger.java index 8e12a39902..db6155d48b 100644 --- a/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebugger.java +++ b/smack-debug/src/main/java/org/jivesoftware/smackx/debugger/EnhancedDebugger.java @@ -206,7 +206,7 @@ private void createDebug() { // the GUI. This is what we call "interpreted" packet data, since it's the packet // data as Smack sees it and not as it's coming in as raw XML. packetReaderListener = new StanzaListener() { - SimpleDateFormat dateFormatter = new SimpleDateFormat("hh:mm:ss:SS aaa"); + SimpleDateFormat dateFormatter = new SimpleDateFormat("HH:mm:ss:SS"); public void processPacket(final Stanza packet) { SwingUtilities.invokeLater(new Runnable() { @@ -221,7 +221,7 @@ public void run() { // Create a thread that will listen for all outgoing packets and write them to // the GUI. packetWriterListener = new StanzaListener() { - SimpleDateFormat dateFormatter = new SimpleDateFormat("hh:mm:ss:SS aaa"); + SimpleDateFormat dateFormatter = new SimpleDateFormat("HH:mm:ss:SS"); public void processPacket(final Stanza packet) { SwingUtilities.invokeLater(new Runnable() { @@ -669,7 +669,7 @@ private void addInformationPanel() { connPanel.add( label, new GridBagConstraints(0, 3, 1, 1, 0.0, 0.0, 21, 0, new Insets(0, 0, 0, 0), 0, 0)); - field = new JFormattedTextField(new SimpleDateFormat("yyyy.MM.dd hh:mm:ss:SS aaa")); + field = new JFormattedTextField(new SimpleDateFormat("yyyy.MM.dd HH:mm:ss:SS")); field.setMinimumSize(new java.awt.Dimension(150, 20)); field.setMaximumSize(new java.awt.Dimension(150, 20)); field.setValue(creationTime); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/IoTControlManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/IoTControlManager.java index 2f347edeb5..a1fd4f7c95 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/IoTControlManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/IoTControlManager.java @@ -123,7 +123,7 @@ public IoTSetResponse setUsingIq(FullJid jid, SetData data) throws NoResponseExc * @throws NotConnectedException * @throws InterruptedException */ - public IoTSetResponse setUsingIq(FullJid jid, Collection data) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + public IoTSetResponse setUsingIq(FullJid jid, Collection data) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { IoTSetRequest request = new IoTSetRequest(data); request.setTo(jid); IoTSetResponse response = connection().createPacketCollectorAndSend(request).nextResultOrThrow(); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/element/IoTSetRequest.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/element/IoTSetRequest.java index dca7293e4d..b77787f985 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/element/IoTSetRequest.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/control/element/IoTSetRequest.java @@ -28,7 +28,7 @@ public class IoTSetRequest extends IQ { private final Collection setData; - public IoTSetRequest(Collection setData) { + public IoTSetRequest(Collection setData) { super(ELEMENT, NAMESPACE); setType(Type.set); this.setData = Collections.unmodifiableCollection(setData); diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/BecameFriendListener.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/BecameFriendListener.java new file mode 100644 index 0000000000..83d9f49fe9 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/BecameFriendListener.java @@ -0,0 +1,26 @@ +/** + * + * Copyright 2016 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.iot.provisioning; + +import org.jivesoftware.smack.packet.Presence; +import org.jxmpp.jid.BareJid; + +public interface BecameFriendListener { + + void becameFriend(BareJid jid, Presence presence); + +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/IoTProvisioningManager.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/IoTProvisioningManager.java index d9264dfca1..1ef6a68114 100644 --- a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/IoTProvisioningManager.java +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/IoTProvisioningManager.java @@ -18,7 +18,9 @@ import java.util.List; import java.util.Map; +import java.util.Set; import java.util.WeakHashMap; +import java.util.concurrent.CopyOnWriteArraySet; import java.util.logging.Level; import java.util.logging.Logger; @@ -41,6 +43,7 @@ import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.roster.AbstractPresenceEventListener; import org.jivesoftware.smack.roster.Roster; import org.jivesoftware.smack.roster.RosterEntry; import org.jivesoftware.smack.roster.SubscribeListener; @@ -50,6 +53,7 @@ import org.jivesoftware.smackx.iot.provisioning.element.ClearCache; import org.jivesoftware.smackx.iot.provisioning.element.ClearCacheResponse; import org.jivesoftware.smackx.iot.provisioning.element.Constants; +import org.jivesoftware.smackx.iot.provisioning.element.Friend; import org.jivesoftware.smackx.iot.provisioning.element.IoTIsFriend; import org.jivesoftware.smackx.iot.provisioning.element.IoTIsFriendResponse; import org.jivesoftware.smackx.iot.provisioning.element.Unfriend; @@ -68,6 +72,8 @@ public final class IoTProvisioningManager extends Manager { private static final Logger LOGGER = Logger.getLogger(IoTProvisioningManager.class.getName()); + private static final StanzaFilter FRIEND_MESSAGE = new AndFilter(StanzaTypeFilter.MESSAGE, + new StanzaExtensionFilter(Friend.ELEMENT, Friend.NAMESPACE)); private static final StanzaFilter UNFRIEND_MESSAGE = new AndFilter(StanzaTypeFilter.MESSAGE, new StanzaExtensionFilter(Unfriend.ELEMENT, Unfriend.NAMESPACE)); @@ -99,6 +105,13 @@ public static synchronized IoTProvisioningManager getInstanceFor(XMPPConnection private final Roster roster; private final LruCache> negativeFriendshipRequestCache = new LruCache<>(8); + private final LruCache friendshipDeniedCache = new LruCache<>(16); + + private final LruCache friendshipRequestedCache = new LruCache<>(16); + + private final Set becameFriendListeners = new CopyOnWriteArraySet<>(); + + private final Set wasUnfriendedListeners = new CopyOnWriteArraySet<>(); private Jid configuredProvisioningServer; @@ -129,6 +142,47 @@ public void processPacket(Stanza stanza) throws NotConnectedException, Interrupt } }, UNFRIEND_MESSAGE); + // Stanza listener for XEP-0324 § 3.2.4. + connection.addAsyncStanzaListener(new StanzaListener() { + @Override + public void processPacket(final Stanza stanza) throws NotConnectedException, InterruptedException { + final Message friendMessage = (Message) stanza; + final Friend friend = Friend.from(friendMessage); + final BareJid friendJid = friend.getFriend(); + + if (isFromProvisioningService(friendMessage)) { + // We received a recommendation from a provisioning server. + // Notify the recommended friend that we will now accept his + // friendship requests. + final XMPPConnection connection = connection(); + Friend friendNotifiacation = new Friend(connection.getUser().asBareJid()); + Message notificationMessage = new Message(friendJid, friendNotifiacation); + connection.sendStanza(notificationMessage); + } else { + // Check is the message was send from a thing we previously + // tried to become friends with. If this is the case, then + // thing is likely telling us that we can become now + // friends. + Jid from = friendMessage.getFrom(); + if (!friendshipDeniedCache.containsKey(from)) { + return; + } + + BareJid bareFrom = from.asBareJid(); + // Sanity check: If a thing recommends us itself as friend, + // which should be the case once we reach this code, then + // the bare 'from' JID should be equals to the JID of the + // recommended friend. + if (!bareFrom.equals(friendJid)) { + return; + } + + // Re-try the friendship request. + sendFriendshipRequest(friendJid); + } + } + }, FRIEND_MESSAGE); + connection.registerIQRequestHandler( new AbstractIqRequestHandler(ClearCache.ELEMENT, ClearCache.NAMESPACE, Type.set, Mode.async) { @Override @@ -193,6 +247,25 @@ public SubscribeAnswer processSubscribe(Jid from, Presence subscribeRequest) { } } }); + + roster.addPresenceEventListener(new AbstractPresenceEventListener() { + @Override + public void presenceSubscribed(BareJid address, Presence subscribedPresence) { + friendshipRequestedCache.remove(address); + for (BecameFriendListener becameFriendListener : becameFriendListeners) { + becameFriendListener.becameFriend(address, subscribedPresence); + } + } + @Override + public void presenceUnsubscribed(BareJid address, Presence unsubscribedPresence) { + if (friendshipRequestedCache.containsKey(address)) { + friendshipDeniedCache.put(address, null); + } + for (WasUnfriendedListener wasUnfriendedListener : wasUnfriendedListeners) { + wasUnfriendedListener.wasUnfriendedListener(address, unsubscribedPresence); + } + } + }); } /** @@ -272,6 +345,9 @@ public boolean isFriend(Jid provisioningServer, BareJid friendInQuestion) throws public void sendFriendshipRequest(BareJid bareJid) throws NotConnectedException, InterruptedException { Presence presence = new Presence(Presence.Type.subscribe); presence.setTo(bareJid); + + friendshipRequestedCache.put(bareJid, null); + connection().sendStanza(presence); } @@ -295,6 +371,22 @@ public void unfriend(Jid friend) throws NotConnectedException, InterruptedExcept } } + public boolean addBecameFriendListener(BecameFriendListener becameFriendListener) { + return becameFriendListeners.add(becameFriendListener); + } + + public boolean removeBecameFriendListener(BecameFriendListener becameFriendListener) { + return becameFriendListeners.remove(becameFriendListener); + } + + public boolean addWasUnfriendedListener(WasUnfriendedListener wasUnfriendedListener) { + return wasUnfriendedListeners.add(wasUnfriendedListener); + } + + public boolean removeWasUnfriendedListener(WasUnfriendedListener wasUnfriendedListener) { + return wasUnfriendedListeners.remove(wasUnfriendedListener); + } + private boolean isFromProvisioningService(Stanza stanza) { Jid provisioningServer; try { diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/WasUnfriendedListener.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/WasUnfriendedListener.java new file mode 100644 index 0000000000..43e9a6d31c --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/WasUnfriendedListener.java @@ -0,0 +1,26 @@ +/** + * + * Copyright 2016 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.iot.provisioning; + +import org.jivesoftware.smack.packet.Presence; +import org.jxmpp.jid.BareJid; + +public interface WasUnfriendedListener { + + void wasUnfriendedListener(BareJid jid, Presence presence); + +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/element/Friend.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/element/Friend.java new file mode 100644 index 0000000000..224269fbde --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/element/Friend.java @@ -0,0 +1,61 @@ +/** + * + * Copyright © 2016 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.iot.provisioning.element; + +import org.jivesoftware.smack.packet.ExtensionElement; +import org.jivesoftware.smack.packet.Message; +import org.jivesoftware.smack.util.Objects; +import org.jivesoftware.smack.util.XmlStringBuilder; +import org.jxmpp.jid.BareJid; + +public class Friend implements ExtensionElement { + + public static final String ELEMENT = "friend"; + public static final String NAMESPACE = Constants.IOT_PROVISIONING_NAMESPACE; + + private final BareJid friend; + + public Friend(BareJid friend) { + this.friend = Objects.requireNonNull(friend, "Friend must not be null"); + } + + @Override + public String getElementName() { + return ELEMENT; + } + + @Override + public String getNamespace() { + return NAMESPACE; + } + + @Override + public XmlStringBuilder toXML() { + XmlStringBuilder xml = new XmlStringBuilder(this); + xml.attribute("jid", friend); + xml.closeEmptyElement(); + return xml; + } + + public BareJid getFriend() { + return friend; + } + + public static Friend from(Message message) { + return message.getExtension(ELEMENT, NAMESPACE); + } +} diff --git a/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/provider/FriendProvider.java b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/provider/FriendProvider.java new file mode 100644 index 0000000000..7267a75ad6 --- /dev/null +++ b/smack-experimental/src/main/java/org/jivesoftware/smackx/iot/provisioning/provider/FriendProvider.java @@ -0,0 +1,34 @@ +/** + * + * Copyright © 2016 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.iot.provisioning.provider; + +import org.jivesoftware.smack.provider.ExtensionElementProvider; +import org.jivesoftware.smack.util.ParserUtils; +import org.jivesoftware.smackx.iot.provisioning.element.Friend; +import org.jxmpp.jid.BareJid; +import org.jxmpp.stringprep.XmppStringprepException; +import org.xmlpull.v1.XmlPullParser; + +public class FriendProvider extends ExtensionElementProvider { + + @Override + public Friend parse(XmlPullParser parser, int initialDepth) throws XmppStringprepException { + BareJid jid = ParserUtils.getBareJidAttribute(parser); + return new Friend(jid); + } + +} diff --git a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers index 145309c84f..a88e9aae74 100644 --- a/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers +++ b/smack-experimental/src/main/resources/org.jivesoftware.smack.experimental/experimental.providers @@ -132,6 +132,11 @@ urn:xmpp:iot:provisioning org.jivesoftware.smackx.iot.provisioning.provider.ClearCacheResponseProvider + + friend + urn:xmpp:iot:provisioning + org.jivesoftware.smackx.iot.provisioning.provider.FriendProvider + unfriend urn:xmpp:iot:provisioning @@ -166,5 +171,5 @@ urn:xmpp:iot:control org.jivesoftware.smackx.iot.control.provider.IoTSetResponseProvider - + diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/BlockingCommandManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/BlockingCommandManager.java new file mode 100644 index 0000000000..6ee6bdce7d --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/BlockingCommandManager.java @@ -0,0 +1,221 @@ +/** + * + * Copyright 2016 Fernando Ramirez, Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.blocking; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +import org.jivesoftware.smack.AbstractConnectionListener; +import org.jivesoftware.smack.ConnectionCreationListener; +import org.jivesoftware.smack.Manager; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPConnectionRegistry; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; +import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode; +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.IQ.Type; +import org.jivesoftware.smackx.blocking.element.BlockContactsIQ; +import org.jivesoftware.smackx.blocking.element.BlockListIQ; +import org.jivesoftware.smackx.blocking.element.UnblockContactsIQ; +import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; +import org.jxmpp.jid.Jid; + +/** + * Blocking command manager class. + * + * @author Fernando Ramirez + * @author Florian Schmaus + * @see XEP-0191: Blocking + * Command + */ +public final class BlockingCommandManager extends Manager { + + public static final String NAMESPACE = "urn:xmpp:blocking"; + + private volatile List blockListCached; + + static { + XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { + @Override + public void connectionCreated(XMPPConnection connection) { + getInstanceFor(connection); + } + }); + } + + private static final Map INSTANCES = new WeakHashMap<>(); + + /** + * Get the singleton instance of BlockingCommandManager. + * + * @param connection + * @return the instance of BlockingCommandManager + */ + public static synchronized BlockingCommandManager getInstanceFor(XMPPConnection connection) { + BlockingCommandManager blockingCommandManager = INSTANCES.get(connection); + + if (blockingCommandManager == null) { + blockingCommandManager = new BlockingCommandManager(connection); + INSTANCES.put(connection, blockingCommandManager); + } + + return blockingCommandManager; + } + + private BlockingCommandManager(XMPPConnection connection) { + super(connection); + + // block IQ handler + connection.registerIQRequestHandler( + new AbstractIqRequestHandler(BlockContactsIQ.ELEMENT, BlockContactsIQ.NAMESPACE, Type.set, Mode.sync) { + @Override + public IQ handleIQRequest(IQ iqRequest) { + BlockContactsIQ blockContactIQ = (BlockContactsIQ) iqRequest; + + if (blockListCached == null) { + blockListCached = new ArrayList(); + } + + List blockedJids = blockContactIQ.getJids(); + blockListCached.addAll(blockedJids); + + return IQ.createResultIQ(blockContactIQ); + } + }); + + // unblock IQ handler + connection.registerIQRequestHandler(new AbstractIqRequestHandler(UnblockContactsIQ.ELEMENT, + UnblockContactsIQ.NAMESPACE, Type.set, Mode.sync) { + @Override + public IQ handleIQRequest(IQ iqRequest) { + UnblockContactsIQ unblockContactIQ = (UnblockContactsIQ) iqRequest; + + if (blockListCached == null) { + blockListCached = new ArrayList(); + } + + List unblockedJids = unblockContactIQ.getJids(); + if (unblockedJids == null) { // remove all + blockListCached.clear(); + } else { // remove only some + blockListCached.removeAll(unblockedJids); + } + + return IQ.createResultIQ(unblockContactIQ); + } + }); + + connection.addConnectionListener(new AbstractConnectionListener() { + @Override + public void authenticated(XMPPConnection connection, boolean resumed) { + // No need to reset the cache if the connection got resumed. + if (resumed) { + return; + } + blockListCached = null; + } + }); + } + + /** + * Returns true if Blocking Command is supported by the server. + * + * @return true if Blocking Command is supported by the server. + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + */ + public boolean isSupportedByServer() + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + return ServiceDiscoveryManager.getInstanceFor(connection()).serverSupportsFeature(NAMESPACE); + } + + /** + * Returns the block list. + * + * @return the blocking list + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + */ + public List getBlockList() + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + + if (blockListCached != null) { + return Collections.unmodifiableList(blockListCached); + } + + BlockListIQ blockListIQ = new BlockListIQ(); + BlockListIQ blockListIQResult = connection().createPacketCollectorAndSend(blockListIQ).nextResultOrThrow(); + blockListCached = blockListIQResult.getJids(); + + return Collections.unmodifiableList(blockListCached); + } + + /** + * Block contacts. + * + * @param jids + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + */ + public void blockContacts(List jids) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + BlockContactsIQ blockContactIQ = new BlockContactsIQ(jids); + connection().createPacketCollectorAndSend(blockContactIQ).nextResultOrThrow(); + } + + /** + * Unblock contacts. + * + * @param jids + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + */ + public void unblockContacts(List jids) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + UnblockContactsIQ unblockContactIQ = new UnblockContactsIQ(jids); + connection().createPacketCollectorAndSend(unblockContactIQ).nextResultOrThrow(); + } + + /** + * Unblock all. + * + * @throws NoResponseException + * @throws XMPPErrorException + * @throws NotConnectedException + * @throws InterruptedException + */ + public void unblockAll() + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + UnblockContactsIQ unblockContactIQ = new UnblockContactsIQ(); + connection().createPacketCollectorAndSend(unblockContactIQ).nextResultOrThrow(); + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/BlockContactsIQ.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/BlockContactsIQ.java new file mode 100644 index 0000000000..19ca0eb72c --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/BlockContactsIQ.java @@ -0,0 +1,81 @@ +/** + * + * Copyright 2016 Fernando Ramirez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.blocking.element; + +import java.util.List; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.blocking.BlockingCommandManager; +import org.jxmpp.jid.Jid; + +/** + * Block contact IQ class. + * + * @author Fernando Ramirez + * @see XEP-0191: Blocking + * Command + */ +public class BlockContactsIQ extends IQ { + + /** + * block element. + */ + public static final String ELEMENT = "block"; + + /** + * the IQ NAMESPACE. + */ + public static final String NAMESPACE = BlockingCommandManager.NAMESPACE; + + private final List jids; + + /** + * Block list IQ constructor. + * + * @param jids + */ + public BlockContactsIQ(List jids) { + super(ELEMENT, NAMESPACE); + this.setType(Type.set); + this.jids = jids; + } + + /** + * Get the JID. + * + * @return the list of JIDs + */ + public List getJids() { + return jids; + } + + @Override + protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) { + xml.rightAngleBracket(); + + if (jids != null) { + for (Jid jid : jids) { + xml.halfOpenElement("item"); + xml.attribute("jid", jid); + xml.closeEmptyElement(); + } + } + + return xml; + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/BlockListIQ.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/BlockListIQ.java new file mode 100644 index 0000000000..c24da5718e --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/BlockListIQ.java @@ -0,0 +1,93 @@ +/** + * + * Copyright 2016 Fernando Ramirez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.blocking.element; + +import java.util.Collections; +import java.util.List; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.blocking.BlockingCommandManager; +import org.jxmpp.jid.Jid; + +/** + * Block list IQ class. + * + * @author Fernando Ramirez + * @see XEP-0191: Blocking + * Command + */ +public class BlockListIQ extends IQ { + + /** + * block list element. + */ + public static final String ELEMENT = "blocklist"; + + /** + * the IQ NAMESPACE. + */ + public static final String NAMESPACE = BlockingCommandManager.NAMESPACE; + + private final List jids; + + /** + * Block list IQ constructor. + * + * @param jids + */ + public BlockListIQ(List jids) { + super(ELEMENT, NAMESPACE); + if (jids == null) { + jids = Collections.emptyList(); + } + this.jids = jids; + } + + /** + * Block list IQ constructor. + */ + public BlockListIQ() { + this(null); + } + + /** + * Get the JIDs. + * + * @return the JIDs + */ + public List getJids() { + return jids; + } + + @Override + protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) { + if (jids.isEmpty()) { + xml.setEmptyElement(); + } else { + xml.rightAngleBracket(); + + for (Jid jid : jids) { + xml.halfOpenElement("item"); + xml.attribute("jid", jid); + xml.closeEmptyElement(); + } + } + + return xml; + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/UnblockContactsIQ.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/UnblockContactsIQ.java new file mode 100644 index 0000000000..5b2260b37a --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/UnblockContactsIQ.java @@ -0,0 +1,89 @@ +/** + * + * Copyright 2016 Fernando Ramirez, Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.blocking.element; + +import java.util.List; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smackx.blocking.BlockingCommandManager; +import org.jxmpp.jid.Jid; + +/** + * Unblock contact IQ class. + * + * @author Fernando Ramirez + * @author Florian Schmaus + * @see XEP-0191: Blocking + * Command + */ +public class UnblockContactsIQ extends IQ { + + /** + * unblock element. + */ + public static final String ELEMENT = "unblock"; + + /** + * the IQ NAMESPACE. + */ + public static final String NAMESPACE = BlockingCommandManager.NAMESPACE; + + private final List jids; + + /** + * Unblock contacts IQ constructor. + * + * @param jids + */ + public UnblockContactsIQ(List jids) { + super(ELEMENT, NAMESPACE); + this.setType(Type.set); + this.jids = jids; + } + + /** + * Constructs a new unblock IQ which will unblock all JIDs. + */ + public UnblockContactsIQ() { + this(null); + } + + /** + * Get the JIDs. + * + * @return the list of JIDs + */ + public List getJids() { + return jids; + } + + @Override + protected IQChildElementXmlStringBuilder getIQChildElementBuilder(IQChildElementXmlStringBuilder xml) { + xml.rightAngleBracket(); + + if (jids != null) { + for (Jid jid : jids) { + xml.halfOpenElement("item"); + xml.attribute("jid", jid); + xml.closeEmptyElement(); + } + } + + return xml; + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/package-info.java new file mode 100644 index 0000000000..129e93b6a2 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/element/package-info.java @@ -0,0 +1,24 @@ +/** + * + * Copyright 2016 Fernando Ramirez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Blocking command elements. + * + * @author Fernando Ramirez + * @see XEP-0191: Blocking + * Command + */ +package org.jivesoftware.smackx.blocking.element; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/package-info.java new file mode 100644 index 0000000000..8521595e45 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/package-info.java @@ -0,0 +1,24 @@ +/** + * + * Copyright 2016 Fernando Ramirez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Classes and interfaces of Blocking command. + * + * @author Fernando Ramirez + * @see XEP-0191: Blocking + * Command + */ +package org.jivesoftware.smackx.blocking; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/provider/BlockContactsIQProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/provider/BlockContactsIQProvider.java new file mode 100644 index 0000000000..ad45f5c4b2 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/provider/BlockContactsIQProvider.java @@ -0,0 +1,67 @@ +/** + * + * Copyright 2016 Fernando Ramirez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.blocking.provider; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.blocking.element.BlockContactsIQ; +import org.jxmpp.jid.Jid; +import org.jxmpp.jid.impl.JidCreate; +import org.xmlpull.v1.XmlPullParser; + +/** + * Block contact IQ provider class. + * + * @author Fernando Ramirez + * @see XEP-0191: Blocking + * Command + */ +public class BlockContactsIQProvider extends IQProvider { + + @Override + public BlockContactsIQ parse(XmlPullParser parser, int initialDepth) throws Exception { + List jids = null; + + outerloop: while (true) { + int eventType = parser.next(); + + switch (eventType) { + + case XmlPullParser.START_TAG: + if (parser.getName().equals("item")) { + if (jids == null) { + jids = new ArrayList<>(); + } + jids.add(JidCreate.from(parser.getAttributeValue("", "jid"))); + } + break; + + case XmlPullParser.END_TAG: + if (parser.getDepth() == initialDepth) { + break outerloop; + } + break; + + } + } + + return new BlockContactsIQ(jids); + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/provider/BlockListIQProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/provider/BlockListIQProvider.java new file mode 100644 index 0000000000..520c22b527 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/provider/BlockListIQProvider.java @@ -0,0 +1,71 @@ +/** + * + * Copyright 2016 Fernando Ramirez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.blocking.provider; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.packet.IQ.Type; +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smack.util.ParserUtils; +import org.jivesoftware.smackx.blocking.element.BlockListIQ; +import org.jxmpp.jid.Jid; +import org.xmlpull.v1.XmlPullParser; + +/** + * Block list IQ provider class. + * + * @author Fernando Ramirez + * @see XEP-0191: Blocking + * Command + */ +public class BlockListIQProvider extends IQProvider { + + @Override + public BlockListIQ parse(XmlPullParser parser, int initialDepth) throws Exception { + List jids = null; + + outerloop: while (true) { + int eventType = parser.next(); + + switch (eventType) { + + case XmlPullParser.START_TAG: + if (parser.getName().equals("item")) { + if (jids == null) { + jids = new ArrayList<>(); + } + Jid jid = ParserUtils.getJidAttribute(parser); + jids.add(jid); + } + break; + + case XmlPullParser.END_TAG: + if (parser.getDepth() == initialDepth) { + break outerloop; + } + break; + + } + } + + BlockListIQ blockListIQ = new BlockListIQ(jids); + blockListIQ.setType(Type.result); + return blockListIQ; + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/provider/UnblockContactsIQProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/provider/UnblockContactsIQProvider.java new file mode 100644 index 0000000000..976236f137 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/provider/UnblockContactsIQProvider.java @@ -0,0 +1,67 @@ +/** + * + * Copyright 2016 Fernando Ramirez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.blocking.provider; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.provider.IQProvider; +import org.jivesoftware.smackx.blocking.element.UnblockContactsIQ; +import org.jxmpp.jid.Jid; +import org.jxmpp.jid.impl.JidCreate; +import org.xmlpull.v1.XmlPullParser; + +/** + * Unblock contact IQ provider class. + * + * @author Fernando Ramirez + * @see XEP-0191: Blocking + * Command + */ +public class UnblockContactsIQProvider extends IQProvider { + + @Override + public UnblockContactsIQ parse(XmlPullParser parser, int initialDepth) throws Exception { + List jids = null; + + outerloop: while (true) { + int eventType = parser.next(); + + switch (eventType) { + + case XmlPullParser.START_TAG: + if (parser.getName().equals("item")) { + if (jids == null) { + jids = new ArrayList<>(); + } + jids.add(JidCreate.from(parser.getAttributeValue("", "jid"))); + } + break; + + case XmlPullParser.END_TAG: + if (parser.getDepth() == initialDepth) { + break outerloop; + } + break; + + } + } + + return new UnblockContactsIQ(jids); + } + +} diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/provider/package-info.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/provider/package-info.java new file mode 100644 index 0000000000..054954ef72 --- /dev/null +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/blocking/provider/package-info.java @@ -0,0 +1,24 @@ +/** + * + * Copyright 2016 Fernando Ramirez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/** + * Blocking command providers. + * + * @author Fernando Ramirez + * @see XEP-0191: Blocking + * Command + */ +package org.jivesoftware.smackx.blocking.provider; diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java index c34391ed11..b0e759a9b8 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/muc/MultiUserChat.java @@ -316,7 +316,7 @@ private Presence enter(MucEnterConfiguration conf) throws NotConnectedException, connection.addSyncStanzaListener(presenceListener, new AndFilter(fromRoomFilter, StanzaTypeFilter.PRESENCE)); connection.addSyncStanzaListener(subjectListener, new AndFilter(fromRoomFilter, - MessageWithSubjectFilter.INSTANCE)); + MessageWithSubjectFilter.INSTANCE, new NotFilter(MessageTypeFilter.ERROR))); connection.addSyncStanzaListener(declinesListener, new AndFilter(new StanzaExtensionFilter(MUCUser.ELEMENT, MUCUser.NAMESPACE), new NotFilter(MessageTypeFilter.ERROR))); connection.addPacketInterceptor(presenceInterceptor, new AndFilter(new ToFilter(room), diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/ping/PingManager.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/ping/PingManager.java index fe4a7b8609..0171a10956 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/ping/PingManager.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/ping/PingManager.java @@ -377,7 +377,10 @@ public synchronized void pingServerIfNecessary() { res = pingMyServer(false); } catch (InterruptedException | SmackException e) { - LOGGER.log(Level.WARNING, "Exception while pinging server", e); + // Note that we log the connection here, so that it is not GC'ed between the call to isAuthenticated + // a few lines above and the usage of the connection within pingMyServer(). In order to prevent: + // https://community.igniterealtime.org/thread/59369 + LOGGER.log(Level.WARNING, "Exception while pinging server of " + connection, e); res = false; } // stop when we receive a pong back diff --git a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/provider/AffiliationProvider.java b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/provider/AffiliationProvider.java index 4570168744..d32170eab4 100644 --- a/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/provider/AffiliationProvider.java +++ b/smack-extensions/src/main/java/org/jivesoftware/smackx/pubsub/provider/AffiliationProvider.java @@ -53,7 +53,10 @@ else if (node == null && jid != null) { affiliation = new Affiliation(jid, affiliationType, namespace); } else { - throw new SmackException("Invalid affililation"); + throw new SmackException("Invalid affililation. Either one of 'node' or 'jid' must be set" + + ". Node: " + node + + ". Jid: " + jid + + '.'); } return affiliation; } diff --git a/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers b/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers index 2257b04d0b..2cef339a92 100644 --- a/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers +++ b/smack-extensions/src/main/resources/org.jivesoftware.smack.extensions/extensions.providers @@ -514,5 +514,23 @@ urn:xmpp:message-correct:0 org.jivesoftware.smackx.message_correct.provider.MessageCorrectProvider + + + + blocklist + urn:xmpp:blocking + org.jivesoftware.smackx.blocking.provider.BlockListIQProvider + + + block + urn:xmpp:blocking + org.jivesoftware.smackx.blocking.provider.BlockContactsIQProvider + + + unblock + urn:xmpp:blocking + org.jivesoftware.smackx.blocking.provider.UnblockContactsIQProvider + + diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/blocking/BlockContactsIQTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/blocking/BlockContactsIQTest.java new file mode 100644 index 0000000000..2e765b97b9 --- /dev/null +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/blocking/BlockContactsIQTest.java @@ -0,0 +1,59 @@ +/** + * + * Copyright 2016 Fernando Ramirez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.blocking; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.blocking.element.BlockContactsIQ; +import org.junit.Assert; +import org.junit.Test; +import org.jxmpp.jid.Jid; +import org.jxmpp.jid.impl.JidCreate; + +public class BlockContactsIQTest { + + String blockContactIQExample = "" + "" + + "" + "" + "" + ""; + + String blockContactPushIQExample = "" + + "" + "" + + "" + "" + ""; + + @Test + public void checkBlockContactIQStanza() throws Exception { + List jids = new ArrayList<>(); + jids.add(JidCreate.from("romeo@montague.net")); + jids.add(JidCreate.from("pepe@montague.net")); + + BlockContactsIQ blockContactIQ = new BlockContactsIQ(jids); + blockContactIQ.setStanzaId("block1"); + + Assert.assertEquals(blockContactIQExample, blockContactIQ.toXML().toString()); + } + + @Test + public void checkBlockContactPushIQ() throws Exception { + IQ iq = (IQ) PacketParserUtils.parseStanza(blockContactPushIQExample); + BlockContactsIQ blockContactIQ = (BlockContactsIQ) iq; + Assert.assertEquals(JidCreate.from("romeo@montague.net"), blockContactIQ.getJids().get(0)); + Assert.assertEquals(JidCreate.from("pepe@montague.net"), blockContactIQ.getJids().get(1)); + } + +} diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/blocking/GetBlockingListTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/blocking/GetBlockingListTest.java new file mode 100644 index 0000000000..a14381ccff --- /dev/null +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/blocking/GetBlockingListTest.java @@ -0,0 +1,59 @@ +/** + * + * Copyright 2016 Fernando Ramirez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.blocking; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.IQ.Type; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.blocking.element.BlockListIQ; +import org.junit.Assert; +import org.junit.Test; +import org.jxmpp.jid.impl.JidCreate; + +public class GetBlockingListTest { + + String getBlockingListIQExample = "" + + "" + ""; + + String blockListIQExample = "" + "" + + "" + "" + "" + ""; + + String emptyBlockListIQExample = "" + "" + + ""; + + @Test + public void checkGetBlockingListIQStanza() throws Exception { + BlockListIQ getBlockListIQ = new BlockListIQ(null); + getBlockListIQ.setType(Type.get); + getBlockListIQ.setStanzaId("blocklist1"); + Assert.assertEquals(getBlockingListIQExample, getBlockListIQ.toXML().toString()); + } + + @Test + public void checkBlockListIQ() throws Exception { + IQ iq = (IQ) PacketParserUtils.parseStanza(blockListIQExample); + BlockListIQ blockListIQ = (BlockListIQ) iq; + Assert.assertEquals(2, blockListIQ.getJids().size()); + Assert.assertEquals(JidCreate.from("romeo@montague.net"), blockListIQ.getJids().get(0)); + Assert.assertEquals(JidCreate.from("iago@shakespeare.lit"), blockListIQ.getJids().get(1)); + + IQ iq2 = (IQ) PacketParserUtils.parseStanza(emptyBlockListIQExample); + BlockListIQ emptyBlockListIQ = (BlockListIQ) iq2; + Assert.assertEquals(0, emptyBlockListIQ.getJids().size()); + } + +} diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/blocking/UnblockContactsIQTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/blocking/UnblockContactsIQTest.java new file mode 100644 index 0000000000..3d4f8a9432 --- /dev/null +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/blocking/UnblockContactsIQTest.java @@ -0,0 +1,79 @@ +/** + * + * Copyright 2016 Fernando Ramirez + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smackx.blocking; + +import java.util.ArrayList; +import java.util.List; + +import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.util.PacketParserUtils; +import org.jivesoftware.smackx.blocking.element.UnblockContactsIQ; +import org.junit.Assert; +import org.junit.Test; +import org.jxmpp.jid.Jid; +import org.jxmpp.jid.impl.JidCreate; + +public class UnblockContactsIQTest { + + String unblockContactIQExample = "" + "" + + "" + "" + "" + ""; + + String unblockContactPushIQExample = "" + + "" + "" + + "" + "" + ""; + + String unblockAllIQExample = "" + "" + + ""; + + String unblockAllPushIQExample = "" + + "" + ""; + + @Test + public void checkUnblockContactIQStanza() throws Exception { + List jids = new ArrayList<>(); + jids.add(JidCreate.from("romeo@montague.net")); + jids.add(JidCreate.from("pepe@montague.net")); + + UnblockContactsIQ unblockContactIQ = new UnblockContactsIQ(jids); + unblockContactIQ.setStanzaId("unblock1"); + + Assert.assertEquals(unblockContactIQExample, unblockContactIQ.toXML().toString()); + } + + @Test + public void checkUnblockContactPushIQ() throws Exception { + IQ iq = (IQ) PacketParserUtils.parseStanza(unblockContactPushIQExample); + UnblockContactsIQ unblockContactIQ = (UnblockContactsIQ) iq; + Assert.assertEquals(JidCreate.from("romeo@montague.net"), unblockContactIQ.getJids().get(0)); + Assert.assertEquals(JidCreate.from("pepe@montague.net"), unblockContactIQ.getJids().get(1)); + } + + @Test + public void checkUnblockAllIQStanza() throws Exception { + UnblockContactsIQ unblockAllIQ = new UnblockContactsIQ(null); + unblockAllIQ.setStanzaId("unblock2"); + Assert.assertEquals(unblockAllIQExample, unblockAllIQ.toXML().toString()); + } + + @Test + public void checkUnblockAllPushIQ() throws Exception { + IQ iq = (IQ) PacketParserUtils.parseStanza(unblockAllPushIQExample); + UnblockContactsIQ unblockAllIQ = (UnblockContactsIQ) iq; + Assert.assertNull(unblockAllIQ.getJids()); + } + +} diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ProxyTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ProxyTest.java index fd1458167c..f909bebe82 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ProxyTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/bytestreams/socks5/Socks5ProxyTest.java @@ -155,39 +155,6 @@ public void shouldNotReturnMultipleSameAddress() { assertEquals(1, sameCount); } - /** - * There should be only one thread executing the SOCKS5 proxy process. - */ - @Test - public void shouldOnlyStartOneServerThread() { - int threadCount = Thread.activeCount(); - - Socks5Proxy.setLocalSocks5ProxyPort(7890); - Socks5Proxy proxy = Socks5Proxy.getSocks5Proxy(); - proxy.start(); - - assertTrue(proxy.isRunning()); - assertEquals(threadCount + 1, Thread.activeCount()); - - proxy.start(); - - assertTrue(proxy.isRunning()); - assertEquals(threadCount + 1, Thread.activeCount()); - - proxy.stop(); - - assertFalse(proxy.isRunning()); - assertEquals(threadCount, Thread.activeCount()); - - proxy.start(); - - assertTrue(proxy.isRunning()); - assertEquals(threadCount + 1, Thread.activeCount()); - - proxy.stop(); - - } - /** * If the SOCKS5 proxy accepts a connection that is not a SOCKS5 connection it should close the * corresponding socket. diff --git a/smack-extensions/src/test/java/org/jivesoftware/smackx/filetransfer/FileTransferNegotiatorTest.java b/smack-extensions/src/test/java/org/jivesoftware/smackx/filetransfer/FileTransferNegotiatorTest.java index 80354e25a5..94919fa082 100644 --- a/smack-extensions/src/test/java/org/jivesoftware/smackx/filetransfer/FileTransferNegotiatorTest.java +++ b/smack-extensions/src/test/java/org/jivesoftware/smackx/filetransfer/FileTransferNegotiatorTest.java @@ -33,9 +33,6 @@ public class FileTransferNegotiatorTest extends InitExtensions { @Before public void setUp() throws Exception { - // Uncomment this to enable debug output - // SmackConfiguration.DEBUG = true; - connection = new DummyConnection(); connection.connect(); connection.login(); @@ -54,7 +51,7 @@ public void verifyForm() throws Exception { try { fileNeg.negotiateOutgoingTransfer(JidTestUtil.DUMMY_AT_EXAMPLE_ORG, "streamid", "file", 1024, null, 10); } catch (NoResponseException e) { - // Ignore + // We do not expect an answer. This unit test only checks the request sent. } Stanza packet = connection.getSentPacket(); String xml = packet.toXML().toString(); diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java index 52a18dad29..c372d689d8 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/Roster.java @@ -681,6 +681,7 @@ public boolean addSubscribeListener(SubscribeListener subscribeListener) { Objects.requireNonNull(subscribeListener, "SubscribeListener argument must not be null"); if (subscriptionMode != SubscriptionMode.manual) { previousSubscriptionMode = subscriptionMode; + subscriptionMode = SubscriptionMode.manual; } return subscribeListeners.add(subscribeListener); } @@ -1228,6 +1229,7 @@ private void addUpdateEntry(Collection addedEntries, Collection update RosterPacket.Item oldItem = RosterEntry.toRosterItem(oldEntry); if (!oldEntry.equalsDeep(entry) || !item.getGroupNames().equals(oldItem.getGroupNames())) { updatedEntries.add(item.getJid()); + oldEntry.updateItem(item); } else { // Record the entry as unchanged, so that it doesn't end up as deleted entry unchangedEntries.add(item.getJid()); @@ -1515,7 +1517,7 @@ else if (presenceMap.get(key) != null) { } /** - * Handles roster reults as described in RFC 6121 2.1.4 + * Handles Roster results as described in RFC 6121 2.1.4. */ private class RosterResultListener implements StanzaListener { diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterEntry.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterEntry.java index a47f8a6ed5..f0bb40017f 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterEntry.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterEntry.java @@ -28,6 +28,8 @@ import org.jivesoftware.smack.SmackException.NotConnectedException; import org.jivesoftware.smack.XMPPException.XMPPErrorException; import org.jivesoftware.smack.packet.IQ; +import org.jivesoftware.smack.packet.Presence; +import org.jivesoftware.smack.packet.Presence.Type; import org.jivesoftware.smack.roster.packet.RosterPacket; import org.jxmpp.jid.BareJid; @@ -41,7 +43,7 @@ */ public final class RosterEntry extends Manager { - private final RosterPacket.Item item; + private RosterPacket.Item item; final private Roster roster; /** @@ -120,10 +122,9 @@ public synchronized void setName(String name) throws NotConnectedException, NoRe * @param type the subscription type. * @param subscriptionPending TODO */ - void updateState(String name, RosterPacket.ItemType type, boolean subscriptionPending) { - item.setName(name); - item.setItemType(type); - item.setSubscriptionPending(subscriptionPending); + void updateItem(RosterPacket.Item item) { + assert(item != null); + this.item = item; } /** @@ -209,6 +210,18 @@ public boolean canSeeHisPresence() { } } + /** + * Cancel the presence subscription the XMPP entity representing this roster entry has with us. + * + * @throws NotConnectedException + * @throws InterruptedException + * @since 4.2 + */ + public void cancelSubscription() throws NotConnectedException, InterruptedException { + Presence unsubscribed = new Presence(item.getJid(), Type.unsubscribed); + connection().sendStanza(unsubscribed); + } + public String toString() { StringBuilder buf = new StringBuilder(); if (getName() != null) { diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterUtil.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterUtil.java index 7f13a4aff0..bf1a15eff2 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterUtil.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/RosterUtil.java @@ -23,6 +23,9 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.SmackException.NotLoggedInException; +import org.jivesoftware.smack.XMPPConnection; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.Jid; @@ -80,4 +83,32 @@ public void entriesUpdated(Collection addresses) { roster.removeRosterListener(rosterListener); } } + + public static void askForSubscriptionIfRequired(Roster roster, BareJid jid) + throws NotLoggedInException, NotConnectedException, InterruptedException { + RosterEntry entry = roster.getEntry(jid); + if (entry == null || !(entry.canSeeHisPresence() || entry.isSubscriptionPending())) { + roster.sendSubscriptionRequest(jid); + } + } + + public static void ensureNotSubscribedToEachOther(XMPPConnection connectionOne, XMPPConnection connectionTwo) + throws NotConnectedException, InterruptedException { + final Roster rosterOne = Roster.getInstanceFor(connectionOne); + final BareJid jidOne = connectionOne.getUser().asBareJid(); + final Roster rosterTwo = Roster.getInstanceFor(connectionTwo); + final BareJid jidTwo = connectionTwo.getUser().asBareJid(); + + ensureNotSubscribed(rosterOne, jidTwo); + ensureNotSubscribed(rosterTwo, jidOne); + } + + public static void ensureNotSubscribed(Roster roster, BareJid jid) + throws NotConnectedException, InterruptedException { + RosterEntry entry = roster.getEntry(jid); + if (entry != null && entry.canSeeMyPresence()) { + entry.cancelSubscription(); + } + } + } diff --git a/smack-im/src/main/java/org/jivesoftware/smack/roster/packet/RosterPacket.java b/smack-im/src/main/java/org/jivesoftware/smack/roster/packet/RosterPacket.java index f74ec379df..a9bd5810d0 100644 --- a/smack-im/src/main/java/org/jivesoftware/smack/roster/packet/RosterPacket.java +++ b/smack-im/src/main/java/org/jivesoftware/smack/roster/packet/RosterPacket.java @@ -108,6 +108,7 @@ public void setVersion(String version) { * A roster item, which consists of a JID, their name, the type of subscription, and * the groups the roster item belongs to. */ + // TODO Make this class immutable. public static class Item implements NamedElement { /** @@ -124,6 +125,7 @@ public static class Item implements NamedElement { */ private boolean subscriptionPending; + // TODO Make immutable. private String name; private ItemType itemType = ItemType.none; private boolean approved; diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/IoT.java b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/IoT.java index 4ed7c60e01..489d9b424a 100644 --- a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/IoT.java +++ b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/IoT.java @@ -17,9 +17,11 @@ package org.igniterealtime.smack.smackrepl; import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.roster.Roster; import org.jivesoftware.smack.roster.RosterUtil; +import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeoutException; @@ -41,6 +43,7 @@ import org.jivesoftware.smackx.iot.discovery.AbstractThingStateChangeListener; import org.jivesoftware.smackx.iot.discovery.IoTDiscoveryManager; import org.jivesoftware.smackx.iot.discovery.ThingState; +import org.jivesoftware.smackx.iot.provisioning.BecameFriendListener; import org.jivesoftware.smackx.iot.provisioning.IoTProvisioningManager; import org.jxmpp.jid.BareJid; import org.jxmpp.jid.EntityBareJid; @@ -51,24 +54,23 @@ public class IoT { // A 10 minute timeout. private static final long TIMEOUT = 10 * 60 * 1000; + private interface IotScenario { + void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readinThingConnection) throws XMPPException, SmackException, IOException, InterruptedException, TimeoutException, Exception; + } + public static void iotScenario(String dataThingJidString, String dataThingPassword, String readingThingJidString, - String readingThingPassword) - throws TimeoutException, Exception { + String readingThingPassword, IotScenario scenario) throws TimeoutException, Exception { final EntityBareJid dataThingJid = JidCreate.entityBareFrom(dataThingJidString); final EntityBareJid readingThingJid = JidCreate.entityBareFrom(readingThingJidString); final XMPPTCPConnectionConfiguration dataThingConnectionConfiguration = XMPPTCPConnectionConfiguration.builder() - .setUsernameAndPassword(dataThingJid.getLocalpart(), dataThingPassword) - .setXmppDomain(dataThingJid.asDomainBareJid()) - .setSecurityMode(SecurityMode.disabled) - .setDebuggerEnabled(true) - .build(); - final XMPPTCPConnectionConfiguration readingThingConnectionConfiguration = XMPPTCPConnectionConfiguration.builder() - .setUsernameAndPassword(readingThingJid.getLocalpart(), readingThingPassword) - .setXmppDomain(readingThingJid.asDomainBareJid()) - .setSecurityMode(SecurityMode.disabled) - .setDebuggerEnabled(true) - .build(); + .setUsernameAndPassword(dataThingJid.getLocalpart(), dataThingPassword) + .setXmppDomain(dataThingJid.asDomainBareJid()).setSecurityMode(SecurityMode.disabled) + .setDebuggerEnabled(true).build(); + final XMPPTCPConnectionConfiguration readingThingConnectionConfiguration = XMPPTCPConnectionConfiguration + .builder().setUsernameAndPassword(readingThingJid.getLocalpart(), readingThingPassword) + .setXmppDomain(readingThingJid.asDomainBareJid()).setSecurityMode(SecurityMode.disabled) + .setDebuggerEnabled(true).build(); final XMPPTCPConnection dataThingConnection = new XMPPTCPConnection(dataThingConnectionConfiguration); final XMPPTCPConnection readingThingConnection = new XMPPTCPConnection(readingThingConnectionConfiguration); @@ -76,57 +78,123 @@ public static void iotScenario(String dataThingJidString, String dataThingPasswo dataThingConnection.setPacketReplyTimeout(TIMEOUT); readingThingConnection.setPacketReplyTimeout(TIMEOUT); + dataThingConnection.setUseStreamManagement(false); + readingThingConnection.setUseStreamManagement(false); + try { - iotScenario(dataThingConnection, readingThingConnection); - } - finally { + dataThingConnection.connect().login(); + readingThingConnection.connect().login(); + scenario.iotScenario(dataThingConnection, readingThingConnection); + } finally { dataThingConnection.disconnect(); readingThingConnection.disconnect(); } } - public static void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readingThingConnection) - throws TimeoutException, Exception { - dataThingConnection.connect().login(); - readingThingConnection.connect().login(); - ThingState dataThingState = actAsDataThing(dataThingConnection); + public static void iotReadOutScenario(String dataThingJidString, String dataThingPassword, String readingThingJidString, + String readingThingPassword) + throws Exception { + iotScenario(dataThingJidString, dataThingPassword, readingThingJidString, readingThingPassword, READ_OUT_SCENARIO); + } - final SimpleResultSyncPoint syncPoint = new SimpleResultSyncPoint(); - dataThingState.setThingStateChangeListener(new AbstractThingStateChangeListener() { - @Override - public void owned(BareJid jid) { - syncPoint.signal(); + public static final IotScenario READ_OUT_SCENARIO = new IotScenario() { + @Override + public void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readingThingConnection) throws TimeoutException, Exception { + ThingState dataThingState = actAsDataThing(dataThingConnection); + + final SimpleResultSyncPoint syncPoint = new SimpleResultSyncPoint(); + dataThingState.setThingStateChangeListener(new AbstractThingStateChangeListener() { + @Override + public void owned(BareJid jid) { + syncPoint.signal(); + } + }); + // Wait until the thing is owned. + syncPoint.waitForResult(TIMEOUT); + printStatus("OWNED - Thing now onwed by " + dataThingState.getOwner()); + + // Make sure things are befriended. + IoTProvisioningManager readingThingProvisioningManager = IoTProvisioningManager.getInstanceFor(readingThingConnection); + readingThingProvisioningManager.sendFriendshipRequestIfRequired(dataThingConnection.getUser().asBareJid()); + + Roster dataThingRoster = Roster.getInstanceFor(dataThingConnection); + RosterUtil.waitUntilOtherEntityIsSubscribed(dataThingRoster, readingThingConnection.getUser().asBareJid(), TIMEOUT); + printStatus("FRIENDSHIP ACCEPTED - Trying to read out data"); + + IoTDataManager readingThingDataManager = IoTDataManager.getInstanceFor(readingThingConnection); + List values = readingThingDataManager.requestMomentaryValuesReadOut(dataThingConnection.getUser()); + if (values.size() != 1) { + throw new IllegalStateException("Unexpected number of values returned: " + values.size()); } - }); - // Wait until the thing is owned. - syncPoint.waitForResult(TIMEOUT); - printStatus("OWNED - Thing now onwed by " + dataThingState.getOwner()); - - // Make sure things are befriended. - IoTProvisioningManager readingThingProvisioningManager = IoTProvisioningManager.getInstanceFor(readingThingConnection); - readingThingProvisioningManager.sendFriendshipRequestIfRequired(dataThingConnection.getUser().asBareJid()); - - Roster dataThingRoster = Roster.getInstanceFor(dataThingConnection); - RosterUtil.waitUntilOtherEntityIsSubscribed(dataThingRoster, readingThingConnection.getUser().asBareJid(), TIMEOUT); - printStatus("FRIENDSHIP ACCEPTED - Trying to read out data"); - - IoTDataManager readingThingDataManager = IoTDataManager.getInstanceFor(readingThingConnection); - List values = readingThingDataManager.requestMomentaryValuesReadOut(dataThingConnection.getUser()); - if (values.size() != 1) { - throw new IllegalStateException("Unexpected number of values returned: " + values.size()); + IoTFieldsExtension field = values.get(0); + printStatus("DATA READ-OUT SUCCESS: " + field.toXML()); + printStatus("IoT SCENARIO FINISHED SUCCESSFULLY"); } - IoTFieldsExtension field = values.get(0); - printStatus("DATA READ-OUT SUCCESS: " + field.toXML()); - printStatus("IoT SCENARIO FINISHED SUCCESSFULLY"); + }; + + public static void iotOwnerApprovesFriendScenario(String dataThingJidString, String dataThingPassword, + String readingThingJidString, String readingThingPassword) throws Exception { + iotScenario(dataThingJidString, dataThingPassword, readingThingJidString, readingThingPassword, + OWNER_APPROVES_FRIEND_SCENARIO); } + public static final IotScenario OWNER_APPROVES_FRIEND_SCENARIO = new IotScenario() { + @Override + public void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readingThingConnection) throws TimeoutException, Exception { + // First ensure that the two XMPP entities are not already subscribed to each other presences. + RosterUtil.ensureNotSubscribedToEachOther(dataThingConnection, readingThingConnection); + + final BareJid dataThingBareJid = dataThingConnection.getUser().asBareJid(); + final BareJid readingThingBareJid = readingThingConnection.getUser().asBareJid(); + final ThingState dataThingState = actAsDataThing(dataThingConnection); + + printStatus("WAITING for 'claimed' notification. Please claim thing now"); + final SimpleResultSyncPoint syncPoint = new SimpleResultSyncPoint(); + dataThingState.setThingStateChangeListener(new AbstractThingStateChangeListener() { + @Override + public void owned(BareJid jid) { + syncPoint.signal(); + } + }); + // Wait until the thing is owned. + syncPoint.waitForResult(TIMEOUT); + printStatus("OWNED - Thing now onwed by " + dataThingState.getOwner()); + + // Now, ReadingThing sends a friendship request to data thing, which + // will proxy the request to its provisioning service, which will + // likely return that both a not friends since the owner did not + // authorize the friendship yet. + final SimpleResultSyncPoint friendshipApprovedSyncPoint = new SimpleResultSyncPoint(); + final IoTProvisioningManager readingThingProvisioningManager = IoTProvisioningManager.getInstanceFor(readingThingConnection); + final BecameFriendListener becameFriendListener = new BecameFriendListener() { + @Override + public void becameFriend(BareJid jid, Presence presence) { + if (jid.equals(dataThingBareJid)) { + friendshipApprovedSyncPoint.signal(); + } + } + }; + readingThingProvisioningManager.addBecameFriendListener(becameFriendListener); + + try { + readingThingProvisioningManager + .sendFriendshipRequestIfRequired(dataThingConnection.getUser().asBareJid()); + friendshipApprovedSyncPoint.waitForResult(TIMEOUT); + } finally { + readingThingProvisioningManager.removeBecameFriendListener(becameFriendListener); + } + + printStatus("FRIENDSHIP APPROVED - ReadingThing " + readingThingBareJid + " is now a friend of DataThing " + dataThingBareJid); + } + }; + private static ThingState actAsDataThing(XMPPTCPConnection connection) throws XMPPException, SmackException, InterruptedException { final String key = StringUtils.randomString(12); final String sn = StringUtils.randomString(12); Thing dataThing = Thing.builder() .setKey(key) .setSerialNumber(sn) - .setManufacturer("Ignite Realtime") + .setManufacturer("IgniteRealtime") .setModel("Smack") .setVersion("0.1") .setMomentaryReadOutRequestHandler(new ThingMomentaryReadOutRequest() { @@ -153,6 +221,7 @@ public static void main(String[] args) throws TimeoutException, Exception { if (args.length != 4) { throw new IllegalArgumentException(); } - iotScenario(args[0], args[1], args[2], args[3]); + iotOwnerApprovesFriendScenario(args[0], args[1], args[2], args[3]); } + } diff --git a/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/XmppTools.java b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/XmppTools.java new file mode 100644 index 0000000000..475329a91d --- /dev/null +++ b/smack-repl/src/main/java/org/igniterealtime/smack/smackrepl/XmppTools.java @@ -0,0 +1,103 @@ +/** + * + * Copyright 2016 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.igniterealtime.smack.smackrepl; + +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; + +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.SmackException.NoResponseException; +import org.jivesoftware.smack.SmackException.NotConnectedException; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.XMPPException.XMPPErrorException; +import org.jivesoftware.smack.tcp.XMPPTCPConnection; +import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; +import org.jivesoftware.smack.util.TLSUtils; +import org.jivesoftware.smackx.iqregister.AccountManager; +import org.jxmpp.jid.DomainBareJid; +import org.jxmpp.jid.impl.JidCreate; +import org.jxmpp.jid.parts.Localpart; + +public class XmppTools { + + public static void main(String[] args) throws SmackException, IOException, XMPPException, InterruptedException, + KeyManagementException, NoSuchAlgorithmException { + boolean one = createAccount("xmpp.foobar.io", "smack1", "smack1"); + boolean two = createAccount("xmpp.foobar.io", "smack2", "smack2"); + // CHECKSTYLE:OFF + System.out.println("Account created: " + one + ' ' + two); + // CHECKSTYLE:ON + } + + public static boolean supportsIbr(String xmppDomain) throws SmackException, IOException, XMPPException, + InterruptedException, KeyManagementException, NoSuchAlgorithmException { + DomainBareJid xmppDomainJid = JidCreate.domainBareFrom(xmppDomain); + return supportsIbr(xmppDomainJid); + } + + public static boolean supportsIbr(DomainBareJid xmppDomain) throws SmackException, IOException, XMPPException, + InterruptedException, KeyManagementException, NoSuchAlgorithmException { + XMPPTCPConnectionConfiguration.Builder configBuilder = XMPPTCPConnectionConfiguration.builder() + .setXmppDomain(xmppDomain); + TLSUtils.acceptAllCertificates(configBuilder); + XMPPTCPConnectionConfiguration config = configBuilder.build(); + XMPPTCPConnection connection = new XMPPTCPConnection(config); + connection.connect(); + try { + return supportsIbr(connection); + } finally { + connection.disconnect(); + } + } + + public static boolean supportsIbr(XMPPConnection connection) + throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { + AccountManager accountManager = AccountManager.getInstance(connection); + return accountManager.supportsAccountCreation(); + } + + public static boolean createAccount(String xmppDomain, String username, String password) + throws KeyManagementException, NoSuchAlgorithmException, SmackException, IOException, XMPPException, + InterruptedException { + DomainBareJid xmppDomainJid = JidCreate.domainBareFrom(xmppDomain); + Localpart localpart = Localpart.from(username); + return createAccount(xmppDomainJid, localpart, password); + } + + public static boolean createAccount(DomainBareJid xmppDomain, Localpart username, String password) + throws KeyManagementException, NoSuchAlgorithmException, SmackException, IOException, XMPPException, + InterruptedException { + XMPPTCPConnectionConfiguration.Builder configBuilder = XMPPTCPConnectionConfiguration.builder() + .setXmppDomain(xmppDomain); + TLSUtils.acceptAllCertificates(configBuilder); + XMPPTCPConnectionConfiguration config = configBuilder.build(); + XMPPTCPConnection connection = new XMPPTCPConnection(config); + connection.connect(); + try { + if (!supportsIbr(connection)) + return false; + + AccountManager accountManager = AccountManager.getInstance(connection); + accountManager.createAccount(username, password); + return true; + } finally { + connection.disconnect(); + } + } +} diff --git a/smack-resolver-dnsjava/src/main/java/org/jivesoftware/smack/util/dns/dnsjava/DNSJavaResolver.java b/smack-resolver-dnsjava/src/main/java/org/jivesoftware/smack/util/dns/dnsjava/DNSJavaResolver.java index 3af2f12b56..21cc028cff 100644 --- a/smack-resolver-dnsjava/src/main/java/org/jivesoftware/smack/util/dns/dnsjava/DNSJavaResolver.java +++ b/smack-resolver-dnsjava/src/main/java/org/jivesoftware/smack/util/dns/dnsjava/DNSJavaResolver.java @@ -1,6 +1,6 @@ /** * - * Copyright 2013-2014 Florian Schmaus + * Copyright 2013-2016 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ */ package org.jivesoftware.smack.util.dns.dnsjava; +import java.net.InetAddress; import java.util.ArrayList; import java.util.List; +import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.initializer.SmackInitializer; import org.jivesoftware.smack.util.DNSUtil; import org.jivesoftware.smack.util.dns.DNSResolver; +import org.jivesoftware.smack.util.dns.HostAddress; import org.jivesoftware.smack.util.dns.SRVRecord; import org.xbill.DNS.Lookup; import org.xbill.DNS.Record; @@ -32,7 +35,7 @@ * This implementation uses the dnsjava implementation for resolving DNS addresses. * */ -public class DNSJavaResolver implements SmackInitializer, DNSResolver { +public class DNSJavaResolver extends DNSResolver implements SmackInitializer { private static DNSJavaResolver instance = new DNSJavaResolver(); @@ -40,11 +43,22 @@ public static DNSResolver getInstance() { return instance; } + public DNSJavaResolver() { + super(false); + } + @Override - public List lookupSRVRecords(String name) throws TextParseException { + protected List lookupSRVRecords0(String name, List failedAddresses, DnssecMode dnssecMode) { List res = new ArrayList(); - Lookup lookup = new Lookup(name, Type.SRV); + Lookup lookup; + try { + lookup = new Lookup(name, Type.SRV); + } + catch (TextParseException e) { + throw new IllegalStateException(e); + } + Record[] recs = lookup.run(); if (recs == null) return res; @@ -57,7 +71,12 @@ public List lookupSRVRecords(String name) throws TextParseException { int priority = srvRecord.getPriority(); int weight = srvRecord.getWeight(); - SRVRecord r = new SRVRecord(host, port, priority, weight); + List hostAddresses = lookupHostAddress0(host, failedAddresses, dnssecMode); + if (hostAddresses == null) { + continue; + } + + SRVRecord r = new SRVRecord(host, port, priority, weight, hostAddresses); res.add(r); } } diff --git a/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java b/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java index e7c831e4e3..7de7b911e1 100644 --- a/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java +++ b/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java @@ -1,6 +1,6 @@ /** * - * Copyright 2013-2014 Florian Schmaus + * Copyright 2013-2016 Florian Schmaus * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ */ package org.jivesoftware.smack.util.dns.javax; +import java.net.InetAddress; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; @@ -27,9 +28,11 @@ import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; +import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.initializer.SmackInitializer; import org.jivesoftware.smack.util.DNSUtil; import org.jivesoftware.smack.util.dns.DNSResolver; +import org.jivesoftware.smack.util.dns.HostAddress; import org.jivesoftware.smack.util.dns.SRVRecord; /** @@ -38,7 +41,7 @@ * @author Florian Schmaus * */ -public class JavaxResolver implements SmackInitializer, DNSResolver { +public class JavaxResolver extends DNSResolver implements SmackInitializer { private static JavaxResolver instance; private static DirContext dirContext; @@ -71,27 +74,42 @@ public static void setup() { DNSUtil.setDNSResolver(getInstance()); } + public JavaxResolver() { + super(false); + } + @Override - public List lookupSRVRecords(String name) throws NamingException { + protected List lookupSRVRecords0(String name, List failedAddresses, DnssecMode dnssecMode) { List res = new ArrayList(); - Attributes dnsLookup = dirContext.getAttributes(name, new String[] { "SRV" }); - Attribute srvAttribute = dnsLookup.get("SRV"); - if (srvAttribute == null) - return res; - @SuppressWarnings("unchecked") - NamingEnumeration srvRecords = (NamingEnumeration) srvAttribute.getAll(); - while (srvRecords.hasMore()) { - String srvRecordString = srvRecords.next(); - String[] srvRecordEntries = srvRecordString.split(" "); - int priority = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 4]); - int port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 2]); - int weight = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 3]); - String host = srvRecordEntries[srvRecordEntries.length - 1]; - - SRVRecord srvRecord = new SRVRecord(host, port, priority, weight); - res.add(srvRecord); + try { + Attributes dnsLookup = dirContext.getAttributes(name, new String[] { "SRV" }); + Attribute srvAttribute = dnsLookup.get("SRV"); + if (srvAttribute == null) + return res; + @SuppressWarnings("unchecked") + NamingEnumeration srvRecords = (NamingEnumeration) srvAttribute.getAll(); + while (srvRecords.hasMore()) { + String srvRecordString = srvRecords.next(); + String[] srvRecordEntries = srvRecordString.split(" "); + int priority = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 4]); + int port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 2]); + int weight = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 3]); + String host = srvRecordEntries[srvRecordEntries.length - 1]; + + List hostAddresses = lookupHostAddress0(host, failedAddresses, dnssecMode); + if (hostAddresses == null) { + continue; + } + + SRVRecord srvRecord = new SRVRecord(host, port, priority, weight, hostAddresses); + res.add(srvRecord); + } + } + catch (NamingException e) { + throw new IllegalStateException(e); } + return res; } diff --git a/smack-resolver-minidns/build.gradle b/smack-resolver-minidns/build.gradle index 812fe8c4ba..8e5c95b9ed 100644 --- a/smack-resolver-minidns/build.gradle +++ b/smack-resolver-minidns/build.gradle @@ -5,6 +5,6 @@ javax.naming API (e.g. Android).""" dependencies { compile project(path: ':smack-core') - compile 'de.measite.minidns:minidns:[0.1,0.2)' + compile 'de.measite.minidns:minidns-hla:0.2.0-beta1' compile "org.jxmpp:jxmpp-util-cache:$jxmppVersion" } diff --git a/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDane.java b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDane.java new file mode 100644 index 0000000000..cb66bb02d4 --- /dev/null +++ b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDane.java @@ -0,0 +1,34 @@ +/** + * + * Copyright 2015-2016 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.dns.minidns; + +import org.jivesoftware.smack.util.DNSUtil; +import org.jivesoftware.smack.util.dns.SmackDaneProvider; +import org.jivesoftware.smack.util.dns.SmackDaneVerifier; + +public class MiniDnsDane implements SmackDaneProvider { + public static final MiniDnsDane INSTANCE = new MiniDnsDane(); + + @Override + public SmackDaneVerifier newInstance() { + return new MiniDnsDaneVerifier(); + } + + public static void setup() { + DNSUtil.setDaneProvider(INSTANCE); + } +} diff --git a/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDaneVerifier.java b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDaneVerifier.java new file mode 100644 index 0000000000..ad63c74a1c --- /dev/null +++ b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDaneVerifier.java @@ -0,0 +1,76 @@ +/** + * + * Copyright 2015-2016 Florian Schmaus + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.smack.util.dns.minidns; + +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.jivesoftware.smack.util.dns.SmackDaneVerifier; + +import de.measite.minidns.dane.DaneVerifier; +import de.measite.minidns.dane.ExpectingTrustManager; + +public class MiniDnsDaneVerifier implements SmackDaneVerifier { + private static final Logger LOGGER = Logger.getLogger(MiniDnsDaneVerifier.class.getName()); + + private static final DaneVerifier VERIFIER = new DaneVerifier(); + + private ExpectingTrustManager expectingTrustManager; + + // Package protected constructor. Use MiniDnsDane.newInstance() to create the verifier. + MiniDnsDaneVerifier() { + } + + @Override + public void init(SSLContext context, KeyManager[] km, X509TrustManager tm, SecureRandom random) throws KeyManagementException { + if (expectingTrustManager != null) { + throw new IllegalStateException("DaneProvider was initialized before. Use newInstance() instead."); + } + expectingTrustManager = new ExpectingTrustManager(tm); + context.init(km, new TrustManager[]{expectingTrustManager}, random); + } + + @Override + public void finish(SSLSocket sslSocket) throws CertificateException { + if (VERIFIER.verify(sslSocket)) { + // DANE verification was the only requirement according to the TLSA RR. We can return here. + return; + } + + // DANE verification was successful, but according to the TLSA RR we also must perform PKIX validation. + if (expectingTrustManager.hasException()) { + // PKIX validation has failed. Throw an exception but close the socket first. + try { + sslSocket.close(); + } catch (IOException e) { + LOGGER.log(Level.FINER, "Closing TLS socket failed", e); + } + throw expectingTrustManager.getException(); + } + } + +} diff --git a/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsResolver.java b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsResolver.java index 6b2b78d360..eda7d7929d 100644 --- a/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsResolver.java +++ b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsResolver.java @@ -16,82 +16,148 @@ */ package org.jivesoftware.smack.util.dns.minidns; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.initializer.SmackInitializer; import org.jivesoftware.smack.util.DNSUtil; import org.jivesoftware.smack.util.dns.DNSResolver; +import org.jivesoftware.smack.util.dns.HostAddress; import org.jivesoftware.smack.util.dns.SRVRecord; -import org.jxmpp.util.cache.ExpirationCache; -import de.measite.minidns.Client; import de.measite.minidns.DNSCache; -import de.measite.minidns.DNSMessage; +import de.measite.minidns.DNSMessage.RESPONSE_CODE; import de.measite.minidns.Question; -import de.measite.minidns.Record; -import de.measite.minidns.Record.CLASS; -import de.measite.minidns.Record.TYPE; -import de.measite.minidns.record.Data; +import de.measite.minidns.cache.LRUCache; +import de.measite.minidns.dnssec.DNSSECClient; +import de.measite.minidns.hla.ResolutionUnsuccessfulException; +import de.measite.minidns.hla.ResolverApi; +import de.measite.minidns.hla.ResolverResult; +import de.measite.minidns.record.A; +import de.measite.minidns.record.AAAA; import de.measite.minidns.record.SRV; +import de.measite.minidns.recursive.ReliableDNSClient; /** - * This implementation uses the minidns implementation for + * This implementation uses the MiniDNS implementation for * resolving DNS addresses. */ -public class MiniDnsResolver implements SmackInitializer, DNSResolver { +public class MiniDnsResolver extends DNSResolver implements SmackInitializer { - private static final long ONE_DAY = 24*60*60*1000; - private static final MiniDnsResolver instance = new MiniDnsResolver(); - private static final ExpirationCache cache = new ExpirationCache(10, ONE_DAY); - private final Client client; + private static final MiniDnsResolver INSTANCE = new MiniDnsResolver(); - public MiniDnsResolver() { - client = new Client(new DNSCache() { + private static final DNSCache CACHE = new LRUCache(128); - @Override - public DNSMessage get(Question question) { - return cache.get(question); - } + private static final ResolverApi DNSSEC_RESOLVER = new ResolverApi(new DNSSECClient(CACHE)); - @Override - public void put(Question question, DNSMessage message) { - long expirationTime = ONE_DAY; - for (Record record : message.getAnswers()) { - if (record.isAnswer(question)) { - expirationTime = record.getTtl(); - break; - } - } - cache.put(question, message, expirationTime); - } + private static final ResolverApi NON_DNSSEC_RESOLVER = new ResolverApi(new ReliableDNSClient(CACHE)); - }); + public static DNSResolver getInstance() { + return INSTANCE; } - public static DNSResolver getInstance() { - return instance; + public MiniDnsResolver() { + super(true); } @Override - public List lookupSRVRecords(String name) { - List res = new LinkedList(); - DNSMessage message = client.query(name, TYPE.SRV, CLASS.IN); - if (message == null) { - return res; + protected List lookupSRVRecords0(final String name, List failedAddresses, DnssecMode dnssecMode) { + final ResolverApi resolver = getResolver(dnssecMode); + + ResolverResult result; + try { + result = resolver.resolve(name, SRV.class); + } catch (IOException e) { + failedAddresses.add(new HostAddress(name, e)); + return null; + } + + // TODO: Use ResolverResult.getResolutionUnsuccessfulException() found in newer MiniDNS versions. + if (!result.wasSuccessful()) { + ResolutionUnsuccessfulException resolutionUnsuccessfulException = getExceptionFrom(result); + failedAddresses.add(new HostAddress(name, resolutionUnsuccessfulException)); + return null; + } + + if (shouldAbortIfNotAuthentic(name, dnssecMode, result, failedAddresses)) { + return null; } - for (Record record : message.getAnswers()) { - Data data = record.getPayload(); - if (!(data instanceof SRV)) { + + List res = new LinkedList(); + for (SRV srv : result.getAnswers()) { + String hostname = srv.name.ace; + List hostAddresses = lookupHostAddress0(hostname, failedAddresses, dnssecMode); + if (hostAddresses == null) { continue; } - SRV srv = (SRV) data; - res.add(new SRVRecord(srv.getName(), srv.getPort(), srv.getPriority(), srv.getWeight())); + + SRVRecord srvRecord = new SRVRecord(hostname, srv.port, srv.priority, srv.weight, hostAddresses); + res.add(srvRecord); } + return res; } + @Override + protected List lookupHostAddress0(final String name, List failedAddresses, DnssecMode dnssecMode) { + final ResolverApi resolver = getResolver(dnssecMode); + + final ResolverResult aResult; + final ResolverResult aaaaResult; + + try { + aResult = resolver.resolve(name, A.class); + aaaaResult = resolver.resolve(name, AAAA.class); + } catch (IOException e) { + failedAddresses.add(new HostAddress(name, e)); + return null; + } + + if (!aResult.wasSuccessful() && !aaaaResult.wasSuccessful()) { + // Both results where not successful. + failedAddresses.add(new HostAddress(name, getExceptionFrom(aResult))); + failedAddresses.add(new HostAddress(name, getExceptionFrom(aaaaResult))); + return null; + } + + if (shouldAbortIfNotAuthentic(name, dnssecMode, aResult, failedAddresses) + || shouldAbortIfNotAuthentic(name, dnssecMode, aaaaResult, failedAddresses)) { + return null; + } + + List inetAddresses = new ArrayList<>(aResult.getAnswers().size() + + aaaaResult.getAnswers().size()); + + for (A a : aResult.getAnswers()) { + InetAddress inetAddress; + try { + inetAddress = InetAddress.getByAddress(a.getIp()); + } + catch (UnknownHostException e) { + continue; + } + inetAddresses.add(inetAddress); + } + for (AAAA aaaa : aaaaResult.getAnswers()) { + InetAddress inetAddress; + try { + inetAddress = InetAddress.getByAddress(name, aaaa.getIp()); + } + catch (UnknownHostException e) { + continue; + } + inetAddresses.add(inetAddress); + } + + return inetAddresses; + } + public static void setup() { DNSUtil.setDNSResolver(getInstance()); } @@ -99,7 +165,43 @@ public static void setup() { @Override public List initialize() { setup(); + MiniDnsDane.setup(); return null; } + private static ResolverApi getResolver(DnssecMode dnssecMode) { + if (dnssecMode == DnssecMode.disabled) { + return NON_DNSSEC_RESOLVER; + } else { + return DNSSEC_RESOLVER; + } + } + + private static boolean shouldAbortIfNotAuthentic(String name, DnssecMode dnssecMode, + ResolverResult result, List failedAddresses) { + switch (dnssecMode) { + case needsDnssec: + case needsDnssecAndDane: + // Check if the result is authentic data, i.e. there a no reasons the result is unverified. + // TODO: Use ResolverResult.getDnssecResultNotAuthenticException() of newer MiniDNS versions. + if (!result.isAuthenticData()) { + Exception exception = new Exception("DNSSEC verification failed: " + result.getUnverifiedReasons().iterator().next().getReasonString()); + failedAddresses.add(new HostAddress(name, exception)); + return true; + } + break; + case disabled: + break; + default: + throw new IllegalStateException("Unknown DnssecMode: " + dnssecMode); + } + return false; + } + + private static ResolutionUnsuccessfulException getExceptionFrom(ResolverResult result) { + Question question = result.getQuestion(); + RESPONSE_CODE responseCode = result.getResponseCode(); + ResolutionUnsuccessfulException resolutionUnsuccessfulException = new ResolutionUnsuccessfulException(question, responseCode); + return resolutionUnsuccessfulException; + } } diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java index c40a11c582..d80ba1f66d 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java @@ -29,20 +29,18 @@ import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; -import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.Provider; +import java.security.SecureRandom; import java.security.Security; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; @@ -64,6 +62,8 @@ import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.PasswordCallback; @@ -71,6 +71,7 @@ import org.jivesoftware.smack.AbstractConnectionListener; import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.SmackConfiguration; import org.jivesoftware.smack.SmackException; @@ -124,11 +125,14 @@ import org.jivesoftware.smack.sm.provider.ParseStreamManagement; import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown; import org.jivesoftware.smack.util.Async; +import org.jivesoftware.smack.util.DNSUtil; import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.TLSUtils; import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smack.util.dns.HostAddress; +import org.jivesoftware.smack.util.dns.SmackDaneProvider; +import org.jivesoftware.smack.util.dns.SmackDaneVerifier; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Resourcepart; import org.jxmpp.stringprep.XmppStringprepException; @@ -573,20 +577,9 @@ private void connectUsingConfiguration() throws ConnectionException, IOException String host = hostAddress.getFQDN(); int port = hostAddress.getPort(); if (proxyInfo == null) { - try { - inetAddresses = Arrays.asList(InetAddress.getAllByName(host)).iterator(); - if (!inetAddresses.hasNext()) { - // This should not happen - LOGGER.warning("InetAddress.getAllByName() returned empty result array."); - throw new UnknownHostException(host); - } - } catch (UnknownHostException e) { - hostAddress.setException(e); - // TODO: Change to emptyIterator() once Smack's minimum Android SDK level is >= 19. - List emptyInetAddresses = Collections.emptyList(); - inetAddresses = emptyInetAddresses.iterator(); - continue; - } + inetAddresses = hostAddress.getInetAddresses().iterator(); + assert(inetAddresses.hasNext()); + innerloop: while (inetAddresses.hasNext()) { // Create a *new* Socket before every connection attempt, i.e. connect() call, since Sockets are not // re-usable after a failed connection attempt. See also SMACK-724. @@ -703,6 +696,18 @@ private void proceedTLSReceived() throws NoSuchAlgorithmException, CertificateEx KeyStore ks = null; KeyManager[] kms = null; PasswordCallback pcb = null; + SmackDaneVerifier daneVerifier = null; + + if (config.getDnssecMode() == DnssecMode.needsDnssecAndDane) { + SmackDaneProvider daneProvider = DNSUtil.getDaneProvider(); + if (daneProvider == null) { + throw new UnsupportedOperationException("DANE enabled but no SmackDaneProvider configured"); + } + daneVerifier = daneProvider.newInstance(); + if (daneVerifier == null) { + throw new IllegalStateException("DANE requested but DANE provider did not return a DANE verifier"); + } + } if (context == null) { final String keyStoreType = config.getKeystoreType(); @@ -743,6 +748,8 @@ else if (keyStoreType != null){ LOGGER.log(Level.WARNING, "Exception", e); ks = null; } + } else { + ks.load(null, null); } } @@ -765,7 +772,20 @@ else if (keyStoreType != null){ // If the user didn't specify a SSLContext, use the default one context = SSLContext.getInstance("TLS"); - context.init(kms, null, new java.security.SecureRandom()); + + final SecureRandom secureRandom = new java.security.SecureRandom(); + X509TrustManager customTrustManager = config.getCustomX509TrustManager(); + + if (daneVerifier != null) { + // User requested DANE verification. + daneVerifier.init(context, kms, customTrustManager, secureRandom); + } else { + TrustManager[] customTrustManagers = null; + if (customTrustManager != null) { + customTrustManagers = new TrustManager[] { customTrustManager }; + } + context.init(kms, customTrustManagers, secureRandom); + } } Socket plain = socket; @@ -785,6 +805,10 @@ else if (keyStoreType != null){ // Proceed to do the handshake sslSocket.startHandshake(); + if (daneVerifier != null) { + daneVerifier.finish(sslSocket); + } + final HostnameVerifier verifier = getConfiguration().getHostnameVerifier(); if (verifier == null) { throw new IllegalStateException("No HostnameVerifier set. Use connectionConfiguration.setHostnameVerifier() to configure.");