001/** 002 * 003 * Copyright 2015-2020 Florian Schmaus. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smack.tcp.rce; 018 019import java.net.InetAddress; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.List; 024import java.util.logging.Level; 025import java.util.logging.Logger; 026 027import org.jivesoftware.smack.ConnectionConfiguration; 028import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; 029import org.jivesoftware.smack.datatypes.UInt16; 030import org.jivesoftware.smack.util.DNSUtil; 031import org.jivesoftware.smack.util.dns.DNSResolver; 032import org.jivesoftware.smack.util.rce.RemoteConnectionEndpoint; 033import org.jivesoftware.smack.util.rce.RemoteConnectionEndpointLookupFailure; 034 035import org.minidns.dnsname.DnsName; 036import org.minidns.record.InternetAddressRR; 037import org.minidns.record.SRV; 038import org.minidns.util.SrvUtil; 039 040public class RemoteXmppTcpConnectionEndpoints { 041 042 private static final Logger LOGGER = Logger.getLogger(RemoteXmppTcpConnectionEndpoints.class.getName()); 043 044 public static final String XMPP_CLIENT_DNS_SRV_PREFIX = "_xmpp-client._tcp"; 045 public static final String XMPP_SERVER_DNS_SRV_PREFIX = "_xmpp-server._tcp"; 046 047 /** 048 * Lookups remote connection endpoints on the server for XMPP connections over TCP taking A, AAAA and SRV resource 049 * records into account. If no host address was configured and all lookups failed, for example with NX_DOMAIN, then 050 * result will be populated with the empty list. 051 * 052 * @param config the connection configuration to lookup the endpoints for. 053 * @return a lookup result. 054 */ 055 public static Result<Rfc6120TcpRemoteConnectionEndpoint> lookup(ConnectionConfiguration config) { 056 List<Rfc6120TcpRemoteConnectionEndpoint> discoveredRemoteConnectionEndpoints; 057 List<RemoteConnectionEndpointLookupFailure> lookupFailures; 058 059 final InetAddress hostAddress = config.getHostAddress(); 060 final DnsName host = config.getHost(); 061 062 if (hostAddress != null) { 063 lookupFailures = Collections.emptyList(); 064 065 IpTcpRemoteConnectionEndpoint<InternetAddressRR<?>> connectionEndpoint = IpTcpRemoteConnectionEndpoint.from( 066 hostAddress.toString(), config.getPort(), hostAddress); 067 discoveredRemoteConnectionEndpoints = Collections.singletonList(connectionEndpoint); 068 } else if (host != null) { 069 lookupFailures = new ArrayList<>(1); 070 071 List<InetAddress> hostAddresses = DNSUtil.getDNSResolver().lookupHostAddress(host, 072 lookupFailures, config.getDnssecMode()); 073 074 if (hostAddresses != null) { 075 discoveredRemoteConnectionEndpoints = new ArrayList<>(hostAddresses.size()); 076 UInt16 port = config.getPort(); 077 for (InetAddress inetAddress : hostAddresses) { 078 IpTcpRemoteConnectionEndpoint<InternetAddressRR<?>> connectionEndpoint = IpTcpRemoteConnectionEndpoint.from( 079 host, port, inetAddress); 080 discoveredRemoteConnectionEndpoints.add(connectionEndpoint); 081 } 082 } else { 083 discoveredRemoteConnectionEndpoints = Collections.emptyList(); 084 } 085 } else { 086 lookupFailures = new ArrayList<>(); 087 088 // N.B.: Important to use config.serviceName and not AbstractXMPPConnection.serviceName 089 DnsName dnsName = config.getXmppServiceDomainAsDnsNameIfPossible(); 090 if (dnsName == null) { 091 // TODO: ConnectionConfiguration should check on construction time that either the given XMPP service 092 // name is also a valid DNS name, or that a host is explicitly configured. 093 throw new IllegalStateException(); 094 } 095 discoveredRemoteConnectionEndpoints = resolveXmppServiceDomain(dnsName, lookupFailures, config.getDnssecMode()); 096 } 097 098 // Either the populated host addresses are not empty *or* there must be at least one failed address. 099 assert !discoveredRemoteConnectionEndpoints.isEmpty() || !lookupFailures.isEmpty(); 100 101 return new Result<>(discoveredRemoteConnectionEndpoints, lookupFailures); 102 } 103 104 public static final class Result<RCE extends RemoteConnectionEndpoint> { 105 public final List<RCE> discoveredRemoteConnectionEndpoints; 106 public final List<RemoteConnectionEndpointLookupFailure> lookupFailures; 107 108 private Result(List<RCE> discoveredRemoteConnectionEndpoints, List<RemoteConnectionEndpointLookupFailure> lookupFailures) { 109 this.discoveredRemoteConnectionEndpoints = discoveredRemoteConnectionEndpoints; 110 this.lookupFailures = lookupFailures; 111 } 112 } 113 114 @SuppressWarnings("ImmutableEnumChecker") 115 enum DomainType { 116 server(XMPP_SERVER_DNS_SRV_PREFIX), 117 client(XMPP_CLIENT_DNS_SRV_PREFIX), 118 ; 119 public final DnsName srvPrefix; 120 121 DomainType(String srvPrefixString) { 122 srvPrefix = DnsName.from(srvPrefixString); 123 } 124 } 125 126 /** 127 * Returns a list of HostAddresses under which the specified XMPP server can be reached at for client-to-server 128 * communication. A DNS lookup for a SRV record in the form "_xmpp-client._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 5222. 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 on optional 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> resolveXmppServiceDomain(DnsName domain, 142 List<RemoteConnectionEndpointLookupFailure> lookupFailures, DnssecMode dnssecMode) { 143 DNSResolver dnsResolver = getDnsResolverOrThrow(); 144 return resolveDomain(domain, DomainType.client, lookupFailures, dnssecMode, dnsResolver); 145 } 146 147 /** 148 * Returns a list of HostAddresses under which the specified XMPP server can be reached at for server-to-server 149 * communication. A DNS lookup for a SRV record in the form "_xmpp-server._tcp.example.com" is attempted, according 150 * to section 3.2.1 of RFC 6120. If that lookup fails , it's assumed that the XMPP server lives at the host resolved 151 * by a DNS lookup at the specified domain on the default port of 5269. 152 * <p> 153 * As an example, a lookup for "example.com" may return "im.example.com:5269". 154 * </p> 155 * 156 * @param domain the domain. 157 * @param lookupFailures a list that will be populated with host addresses that failed to resolve. 158 * @param dnssecMode DNSSec mode. 159 * @return List of HostAddress, which encompasses the hostname and port that the 160 * XMPP server can be reached at for the specified domain. 161 */ 162 public static List<Rfc6120TcpRemoteConnectionEndpoint> resolveXmppServerDomain(DnsName domain, 163 List<RemoteConnectionEndpointLookupFailure> lookupFailures, DnssecMode dnssecMode) { 164 DNSResolver dnsResolver = getDnsResolverOrThrow(); 165 return resolveDomain(domain, DomainType.server, lookupFailures, dnssecMode, dnsResolver); 166 } 167 168 /** 169 * 170 * @param domain the domain. 171 * @param domainType the XMPP domain type, server or client. 172 * @param lookupFailures a list that will be populated with all failures that oocured during lookup. 173 * @param dnssecMode the DNSSEC mode. 174 * @param dnsResolver the DNS resolver to use. 175 * @return a list of resolved host addresses for this domain. 176 */ 177 private static List<Rfc6120TcpRemoteConnectionEndpoint> resolveDomain(DnsName domain, DomainType domainType, 178 List<RemoteConnectionEndpointLookupFailure> lookupFailures, DnssecMode dnssecMode, DNSResolver dnsResolver) { 179 List<Rfc6120TcpRemoteConnectionEndpoint> endpoints = new ArrayList<>(); 180 181 // Step one: Do SRV lookups 182 DnsName srvDomain = DnsName.from(domainType.srvPrefix, domain); 183 184 Collection<SRV> srvRecords = dnsResolver.lookupSrvRecords(srvDomain, lookupFailures, dnssecMode); 185 if (srvRecords != null && !srvRecords.isEmpty()) { 186 if (LOGGER.isLoggable(Level.FINE)) { 187 String logMessage = "Resolved SRV RR for " + srvDomain + ":"; 188 for (SRV r : srvRecords) 189 logMessage += " " + r; 190 LOGGER.fine(logMessage); 191 } 192 193 List<SRV> sortedSrvRecords = SrvUtil.sortSrvRecords(srvRecords); 194 195 for (SRV srv : sortedSrvRecords) { 196 List<InetAddress> targetInetAddresses = dnsResolver.lookupHostAddress(srv.target, lookupFailures, dnssecMode); 197 if (targetInetAddresses != null) { 198 SrvXmppRemoteConnectionEndpoint endpoint = new SrvXmppRemoteConnectionEndpoint(srv, targetInetAddresses); 199 endpoints.add(endpoint); 200 } 201 } 202 } else { 203 LOGGER.info("Could not resolve DNS SRV resource records for " + srvDomain + ". Consider adding those."); 204 } 205 206 UInt16 defaultPort; 207 switch (domainType) { 208 case client: 209 defaultPort = UInt16.from(5222); 210 break; 211 case server: 212 defaultPort = UInt16.from(5269); 213 break; 214 default: 215 throw new AssertionError(); 216 } 217 218 // Step two: Add the hostname to the end of the list 219 List<InetAddress> hostAddresses = dnsResolver.lookupHostAddress(domain, lookupFailures, dnssecMode); 220 if (hostAddresses != null) { 221 for (InetAddress inetAddress : hostAddresses) { 222 IpTcpRemoteConnectionEndpoint<InternetAddressRR<?>> endpoint = IpTcpRemoteConnectionEndpoint.from(domain, defaultPort, inetAddress); 223 endpoints.add(endpoint); 224 } 225 } 226 227 return endpoints; 228 } 229 230 private static DNSResolver getDnsResolverOrThrow() { 231 final DNSResolver dnsResolver = DNSUtil.getDNSResolver(); 232 if (dnsResolver == null) { 233 throw new IllegalStateException("No DNS resolver configured in Smack"); 234 } 235 return dnsResolver; 236 } 237}