RemoteXmppTcpConnectionEndpoints.java

  1. /**
  2.  *
  3.  * Copyright 2015-2020 Florian Schmaus.
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smack.tcp.rce;

  18. import java.net.InetAddress;
  19. import java.util.ArrayList;
  20. import java.util.Collection;
  21. import java.util.Collections;
  22. import java.util.List;
  23. import java.util.logging.Level;
  24. import java.util.logging.Logger;

  25. import org.jivesoftware.smack.ConnectionConfiguration;
  26. import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode;
  27. import org.jivesoftware.smack.datatypes.UInt16;
  28. import org.jivesoftware.smack.util.DNSUtil;
  29. import org.jivesoftware.smack.util.dns.DNSResolver;
  30. import org.jivesoftware.smack.util.rce.RemoteConnectionEndpoint;
  31. import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure;

  32. import org.minidns.dnsname.DnsName;
  33. import org.minidns.record.InternetAddressRR;
  34. import org.minidns.record.SRV;
  35. import org.minidns.util.SrvUtil;

  36. public class RemoteXmppTcpConnectionEndpoints {

  37.     private static final Logger LOGGER = Logger.getLogger(RemoteXmppTcpConnectionEndpoints.class.getName());

  38.     public static final String XMPP_CLIENT_DNS_SRV_PREFIX = "_xmpp-client._tcp";
  39.     public static final String XMPP_SERVER_DNS_SRV_PREFIX = "_xmpp-server._tcp";

  40.     /**
  41.      * Lookups remote connection endpoints on the server for XMPP connections over TCP taking A, AAAA and SRV resource
  42.      * records into account. If no host address was configured and all lookups failed, for example with NX_DOMAIN, then
  43.      * result will be populated with the empty list.
  44.      *
  45.      * @param config the connection configuration to lookup the endpoints for.
  46.      * @return a lookup result.
  47.      */
  48.     public static Result<Rfc6120TcpRemoteConnectionEndpoint> lookup(ConnectionConfiguration config) {
  49.         List<Rfc6120TcpRemoteConnectionEndpoint> discoveredRemoteConnectionEndpoints;
  50.         List<RemoteConnectionEndpointLookupFailure> lookupFailures;

  51.         final InetAddress hostAddress = config.getHostAddress();
  52.         final DnsName host = config.getHost();

  53.         if (hostAddress != null) {
  54.             lookupFailures = Collections.emptyList();

  55.             IpTcpRemoteConnectionEndpoint<InternetAddressRR<?>> connectionEndpoint = IpTcpRemoteConnectionEndpoint.from(
  56.                             hostAddress.toString(), config.getPort(), hostAddress);
  57.             discoveredRemoteConnectionEndpoints = Collections.singletonList(connectionEndpoint);
  58.         } else if (host != null) {
  59.             lookupFailures = new ArrayList<>(1);

  60.             List<InetAddress> hostAddresses = DNSUtil.getDNSResolver().lookupHostAddress(host,
  61.                             lookupFailures, config.getDnssecMode());

  62.             if (hostAddresses != null) {
  63.                 discoveredRemoteConnectionEndpoints = new ArrayList<>(hostAddresses.size());
  64.                 UInt16 port = config.getPort();
  65.                 for (InetAddress inetAddress : hostAddresses) {
  66.                     IpTcpRemoteConnectionEndpoint<InternetAddressRR<?>> connectionEndpoint = IpTcpRemoteConnectionEndpoint.from(
  67.                                     host, port, inetAddress);
  68.                     discoveredRemoteConnectionEndpoints.add(connectionEndpoint);
  69.                 }
  70.             } else {
  71.                 discoveredRemoteConnectionEndpoints = Collections.emptyList();
  72.             }
  73.         } else {
  74.             lookupFailures = new ArrayList<>();

  75.             // N.B.: Important to use config.serviceName and not AbstractXMPPConnection.serviceName
  76.             DnsName dnsName = config.getXmppServiceDomainAsDnsNameIfPossible();
  77.             if (dnsName == null) {
  78.                 // TODO: ConnectionConfiguration should check on construction time that either the given XMPP service
  79.                 // name is also a valid DNS name, or that a host is explicitly configured.
  80.                 throw new IllegalStateException();
  81.             }
  82.             discoveredRemoteConnectionEndpoints = resolveXmppServiceDomain(dnsName, lookupFailures, config.getDnssecMode());
  83.         }

  84.         // Either the populated host addresses are not empty *or* there must be at least one failed address.
  85.         assert !discoveredRemoteConnectionEndpoints.isEmpty() || !lookupFailures.isEmpty();

  86.         return new Result<>(discoveredRemoteConnectionEndpoints, lookupFailures);
  87.     }

  88.     public static final class Result<RCE extends RemoteConnectionEndpoint> {
  89.         public final List<RCE> discoveredRemoteConnectionEndpoints;
  90.         public final List<RemoteConnectionEndpointLookupFailure> lookupFailures;

  91.         private Result(List<RCE> discoveredRemoteConnectionEndpoints, List<RemoteConnectionEndpointLookupFailure> lookupFailures) {
  92.             this.discoveredRemoteConnectionEndpoints = discoveredRemoteConnectionEndpoints;
  93.             this.lookupFailures = lookupFailures;
  94.         }
  95.     }

  96.     @SuppressWarnings("ImmutableEnumChecker")
  97.     enum DomainType {
  98.         server(XMPP_SERVER_DNS_SRV_PREFIX),
  99.         client(XMPP_CLIENT_DNS_SRV_PREFIX),
  100.         ;
  101.         public final DnsName srvPrefix;

  102.         DomainType(String srvPrefixString) {
  103.             srvPrefix = DnsName.from(srvPrefixString);
  104.         }
  105.     }

  106.     /**
  107.      * Returns a list of HostAddresses under which the specified XMPP server can be reached at for client-to-server
  108.      * communication. A DNS lookup for a SRV record in the form "_xmpp-client._tcp.example.com" is attempted, according
  109.      * to section 3.2.1 of RFC 6120. If that lookup fails, it's assumed that the XMPP server lives at the host resolved
  110.      * by a DNS lookup at the specified domain on the default port of 5222.
  111.      * <p>
  112.      * As an example, a lookup for "example.com" may return "im.example.com:5269".
  113.      * </p>
  114.      *
  115.      * @param domain the domain.
  116.      * @param lookupFailures on optional list that will be populated with host addresses that failed to resolve.
  117.      * @param dnssecMode DNSSec mode.
  118.      * @return List of HostAddress, which encompasses the hostname and port that the
  119.      *      XMPP server can be reached at for the specified domain.
  120.      */
  121.     public static List<Rfc6120TcpRemoteConnectionEndpoint> resolveXmppServiceDomain(DnsName domain,
  122.                     List<RemoteConnectionEndpointLookupFailure> lookupFailures, DnssecMode dnssecMode) {
  123.         DNSResolver dnsResolver = getDnsResolverOrThrow();
  124.         return resolveDomain(domain, DomainType.client, lookupFailures, dnssecMode, dnsResolver);
  125.     }

  126.     /**
  127.      * Returns a list of HostAddresses under which the specified XMPP server can be reached at for server-to-server
  128.      * communication. A DNS lookup for a SRV record in the form "_xmpp-server._tcp.example.com" is attempted, according
  129.      * to section 3.2.1 of RFC 6120. If that lookup fails , it's assumed that the XMPP server lives at the host resolved
  130.      * by a DNS lookup at the specified domain on the default port of 5269.
  131.      * <p>
  132.      * As an example, a lookup for "example.com" may return "im.example.com:5269".
  133.      * </p>
  134.      *
  135.      * @param domain the domain.
  136.      * @param lookupFailures a list that will be populated with host addresses that failed to resolve.
  137.      * @param dnssecMode DNSSec mode.
  138.      * @return List of HostAddress, which encompasses the hostname and port that the
  139.      *      XMPP server can be reached at for the specified domain.
  140.      */
  141.     public static List<Rfc6120TcpRemoteConnectionEndpoint> resolveXmppServerDomain(DnsName domain,
  142.                     List<RemoteConnectionEndpointLookupFailure> lookupFailures, DnssecMode dnssecMode) {
  143.         DNSResolver dnsResolver = getDnsResolverOrThrow();
  144.         return resolveDomain(domain, DomainType.server, lookupFailures, dnssecMode, dnsResolver);
  145.     }

  146.     /**
  147.      *
  148.      * @param domain the domain.
  149.      * @param domainType the XMPP domain type, server or client.
  150.      * @param lookupFailures a list that will be populated with all failures that occurred during lookup.
  151.      * @param dnssecMode the DNSSEC mode.
  152.      * @param dnsResolver the DNS resolver to use.
  153.      * @return a list of resolved host addresses for this domain.
  154.      */
  155.     private static List<Rfc6120TcpRemoteConnectionEndpoint> resolveDomain(DnsName domain, DomainType domainType,
  156.                     List<RemoteConnectionEndpointLookupFailure> lookupFailures, DnssecMode dnssecMode, DNSResolver dnsResolver) {
  157.         List<Rfc6120TcpRemoteConnectionEndpoint> endpoints = new ArrayList<>();

  158.         // Step one: Do SRV lookups
  159.         DnsName srvDomain = DnsName.from(domainType.srvPrefix, domain);

  160.         Collection<SRV> srvRecords = dnsResolver.lookupSrvRecords(srvDomain, lookupFailures, dnssecMode);
  161.         if (srvRecords != null && !srvRecords.isEmpty()) {
  162.             if (LOGGER.isLoggable(Level.FINE)) {
  163.                 String logMessage = "Resolved SRV RR for " + srvDomain + ":";
  164.                 for (SRV r : srvRecords)
  165.                     logMessage += " " + r;
  166.                 LOGGER.fine(logMessage);
  167.             }

  168.             List<SRV> sortedSrvRecords = SrvUtil.sortSrvRecords(srvRecords);

  169.             for (SRV srv : sortedSrvRecords) {
  170.                 List<InetAddress> targetInetAddresses = dnsResolver.lookupHostAddress(srv.target, lookupFailures, dnssecMode);
  171.                 if (targetInetAddresses != null) {
  172.                     SrvXmppRemoteConnectionEndpoint endpoint = new SrvXmppRemoteConnectionEndpoint(srv, targetInetAddresses);
  173.                     endpoints.add(endpoint);
  174.                 }
  175.             }
  176.         } else {
  177.             LOGGER.info("Could not resolve DNS SRV resource records for " + srvDomain + ". Consider adding those.");
  178.         }

  179.         UInt16 defaultPort;
  180.         switch (domainType) {
  181.         case client:
  182.             defaultPort = UInt16.from(5222);
  183.             break;
  184.         case server:
  185.             defaultPort = UInt16.from(5269);
  186.             break;
  187.         default:
  188.             throw new AssertionError();
  189.         }

  190.         // Step two: Add the hostname to the end of the list
  191.         List<InetAddress> hostAddresses = dnsResolver.lookupHostAddress(domain, lookupFailures, dnssecMode);
  192.         if (hostAddresses != null) {
  193.             for (InetAddress inetAddress : hostAddresses) {
  194.                 IpTcpRemoteConnectionEndpoint<InternetAddressRR<?>> endpoint = IpTcpRemoteConnectionEndpoint.from(domain, defaultPort, inetAddress);
  195.                 endpoints.add(endpoint);
  196.             }
  197.         }

  198.         return endpoints;
  199.     }

  200.     private static DNSResolver getDnsResolverOrThrow() {
  201.         final DNSResolver dnsResolver = DNSUtil.getDNSResolver();
  202.         if (dnsResolver == null) {
  203.             throw new IllegalStateException("No DNS resolver configured in Smack");
  204.         }
  205.         return dnsResolver;
  206.     }
  207. }