// Copyright 2017 The Nomulus Authors. All Rights Reserved. // // 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 google.registry.whois; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Strings.isNullOrEmpty; import static google.registry.model.registry.Registries.findTldForName; import static google.registry.util.DomainNameUtils.canonicalizeDomainName; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import com.google.common.base.Joiner; import com.google.common.flogger.FluentLogger; import com.google.common.io.CharStreams; import com.google.common.net.InetAddresses; import com.google.common.net.InternetDomainName; import google.registry.config.RegistryConfig.Config; import java.io.IOException; import java.io.Reader; import java.util.ArrayList; import java.util.List; import java.util.Optional; import javax.inject.Inject; import org.joda.time.DateTime; /** * The WhoisReader class understands how to read the WHOIS command from some source, parse it, and * produce a new WhoisCommand instance. The command syntax of WHOIS is generally undefined, so we * adopt the following rules: * *
*
domain <FQDN>
* Looks up the domain record for the fully qualified domain name. *
nameserver <FQDN>
* Looks up the nameserver record for the fully qualified domain name. *
nameserver <IP>
* Looks up the nameserver record at the given IP address. *
registrar <IANA ID>
* Looks up the registrar record with the given IANA ID. *
registrar <NAME>
* Looks up the registrar record with the given name. *
<IP>
* Looks up the nameserver record with the given IP address. *
<FQDN>
* Looks up the nameserver or domain record for the fully qualified domain name. *
<IANA ID>
* Looks up the registrar record with the given IANA ID. *
* * @see RFC 3912 * @see IANA Registrar IDs */ class WhoisReader { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); /** * These are strings that will always trigger a specific query type when they are sent at * the beginning of a command. */ static final String DOMAIN_LOOKUP_COMMAND = "domain"; static final String NAMESERVER_LOOKUP_COMMAND = "nameserver"; static final String REGISTRAR_LOOKUP_COMMAND = "registrar"; private final WhoisCommandFactory commandFactory; /** Creates a new WhoisReader that extracts its command from the specified Reader. */ @Inject WhoisReader(@Config("whoisCommandFactory") WhoisCommandFactory commandFactory) { this.commandFactory = commandFactory; } /** * Read a command from some source to produce a new instance of WhoisCommand. * * @throws IOException If the command could not be read from the reader. * @throws WhoisException If the command could not be parsed as a WhoisCommand. */ WhoisCommand readCommand(Reader reader, boolean fullOutput, DateTime now) throws IOException, WhoisException { return parseCommand(CharStreams.toString(checkNotNull(reader, "reader")), fullOutput, now); } /** * Given a WHOIS command string, parse it into its command type and target string. See class level * comments for a full description of the command syntax accepted. */ private WhoisCommand parseCommand(String command, boolean fullOutput, DateTime now) throws WhoisException { // Split the string into tokens based on whitespace. List tokens = filterEmptyStrings(command.split("\\s")); if (tokens.isEmpty()) { throw new WhoisException(now, SC_BAD_REQUEST, "No WHOIS command specified."); } final String arg1 = tokens.get(0); // Check if the first token is equal to the domain lookup command. if (arg1.equalsIgnoreCase(DOMAIN_LOOKUP_COMMAND)) { if (tokens.size() != 2) { throw new WhoisException(now, SC_BAD_REQUEST, String.format( "Wrong number of arguments to '%s' command.", DOMAIN_LOOKUP_COMMAND)); } // Try to parse the argument as a domain name. try { logger.atInfo().log("Attempting domain lookup command using domain name %s", tokens.get(1)); return commandFactory.domainLookup( InternetDomainName.from(canonicalizeDomainName(tokens.get(1))), fullOutput); } catch (IllegalArgumentException iae) { // If we can't interpret the argument as a host name, then return an error. throw new WhoisException(now, SC_BAD_REQUEST, String.format( "Could not parse argument to '%s' command", DOMAIN_LOOKUP_COMMAND)); } } // Check if the first token is equal to the nameserver lookup command. if (arg1.equalsIgnoreCase(NAMESERVER_LOOKUP_COMMAND)) { if (tokens.size() != 2) { throw new WhoisException(now, SC_BAD_REQUEST, String.format( "Wrong number of arguments to '%s' command.", NAMESERVER_LOOKUP_COMMAND)); } // Try to parse the argument as an IP address. try { logger.atInfo().log( "Attempting nameserver lookup command using %s as an IP address", tokens.get(1)); return commandFactory.nameserverLookupByIp(InetAddresses.forString(tokens.get(1))); } catch (IllegalArgumentException iae) { // Silently ignore this exception. } // Try to parse the argument as a host name. try { logger.atInfo().log( "Attempting nameserver lookup command using %s as a hostname", tokens.get(1)); return commandFactory.nameserverLookupByHost(InternetDomainName.from( canonicalizeDomainName(tokens.get(1)))); } catch (IllegalArgumentException iae) { // Silently ignore this exception. } // If we can't interpret the argument as either a host name or IP address, return an error. throw new WhoisException(now, SC_BAD_REQUEST, String.format( "Could not parse argument to '%s' command", NAMESERVER_LOOKUP_COMMAND)); } // Check if the first token is equal to the registrar lookup command. if (arg1.equalsIgnoreCase(REGISTRAR_LOOKUP_COMMAND)) { if (tokens.size() == 1) { throw new WhoisException(now, SC_BAD_REQUEST, String.format( "Too few arguments to '%s' command.", REGISTRAR_LOOKUP_COMMAND)); } String registrarLookupArgument = Joiner.on(' ').join(tokens.subList(1, tokens.size())); logger.atInfo().log( "Attempting registrar lookup command using registrar %s", registrarLookupArgument); return commandFactory.registrarLookup(registrarLookupArgument); } // If we have a single token, then try to interpret that in various ways. if (tokens.size() == 1) { // Try to parse it as an IP address. If successful, then this is a lookup on a nameserver. try { logger.atInfo().log("Attempting nameserver lookup using %s as an IP address", arg1); return commandFactory.nameserverLookupByIp(InetAddresses.forString(arg1)); } catch (IllegalArgumentException iae) { // Silently ignore this exception. } // Try to parse it as a domain name or host name. try { final InternetDomainName targetName = InternetDomainName.from(canonicalizeDomainName(arg1)); // We don't know at this point whether we have a domain name or a host name. We have to // search through our configured TLDs to see if there's one that prefixes the name. Optional tld = findTldForName(targetName); if (!tld.isPresent()) { // This target is not under any configured TLD, so just try it as a registrar name. logger.atInfo().log("Attempting registrar lookup using %s as a registrar", arg1); return new RegistrarLookupCommand(arg1); } // If the target is exactly one level above the TLD, then this is a second level domain // (SLD) and we should do a domain lookup on it. if (targetName.parent().equals(tld.get())) { logger.atInfo().log("Attempting domain lookup using %s as a domain name", targetName); return commandFactory.domainLookup(targetName, fullOutput); } // The target is more than one level above the TLD, so we'll assume it's a nameserver. logger.atInfo().log("Attempting nameserver lookup using %s as a hostname", targetName); return commandFactory.nameserverLookupByHost(targetName); } catch (IllegalArgumentException e) { // Silently ignore this exception. } // Purposefully fall through to code below. } // The only case left is that there are multiple tokens with no particular command given. We'll // assume this is a registrar lookup, since there's really nothing else it could be. String registrarLookupArgument = Joiner.on(' ').join(tokens); logger.atInfo().log( "Attempting registrar lookup employing %s as a registrar", registrarLookupArgument); return commandFactory.registrarLookup(registrarLookupArgument); } /** Returns an ArrayList containing the contents of the String array minus any empty strings. */ private static List filterEmptyStrings(String[] strings) { List list = new ArrayList<>(strings.length); for (String str : strings) { if (!isNullOrEmpty(str)) { list.add(str); } } return list; } }