Add framework for customizable WHOIS commands

With some additional changes by Ben McIlwain.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=145447136
This commit is contained in:
Justin Graham 2017-01-24 11:46:30 -08:00 committed by Ben McIlwain
parent d3fe6be385
commit bb3a0c78c5
15 changed files with 155 additions and 47 deletions

View file

@ -990,6 +990,13 @@ public final class RegistryConfig {
return "google.registry.flows.custom.CustomLogicFactory"; return "google.registry.flows.custom.CustomLogicFactory";
} }
@Provides
@Config("whoisCommandFactoryClass")
public static String provideWhoisCommandFactoryClass() {
// TODO(b/32875427): This will be converted to YAML configuration in a future refactor.
return "google.registry.whois.WhoisCommandFactory";
}
private static final String RESERVED_TERMS_EXPORT_DISCLAIMER = "" private static final String RESERVED_TERMS_EXPORT_DISCLAIMER = ""
+ "# This list contains reserve terms for the TLD. Other terms may be reserved\n" + "# This list contains reserve terms for the TLD. Other terms may be reserved\n"
+ "# but not included in this list, including terms EXAMPLE REGISTRY chooses not\n" + "# but not included in this list, including terms EXAMPLE REGISTRY chooses not\n"

View file

@ -26,6 +26,7 @@ import google.registry.request.Modules.Jackson2Module;
import google.registry.request.Modules.URLFetchServiceModule; import google.registry.request.Modules.URLFetchServiceModule;
import google.registry.util.SystemClock.SystemClockModule; import google.registry.util.SystemClock.SystemClockModule;
import google.registry.util.SystemSleeper.SystemSleeperModule; import google.registry.util.SystemSleeper.SystemSleeperModule;
import google.registry.whois.WhoisModule;
import javax.inject.Singleton; import javax.inject.Singleton;
/** /**
@ -50,6 +51,7 @@ import javax.inject.Singleton;
SystemSleeperModule.class, SystemSleeperModule.class,
URLFetchServiceModule.class, URLFetchServiceModule.class,
VoidDnsWriterModule.class, VoidDnsWriterModule.class,
WhoisModule.class,
}, },
dependencies = { dependencies = {
HttpRequestFactoryComponent.class, HttpRequestFactoryComponent.class,

View file

@ -14,13 +14,16 @@
package google.registry.whois; package google.registry.whois;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import com.google.common.base.Optional;
import com.google.common.net.InternetDomainName; import com.google.common.net.InternetDomainName;
import google.registry.model.domain.DomainResource; import google.registry.model.domain.DomainResource;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.joda.time.DateTime; import org.joda.time.DateTime;
/** Represents a WHOIS lookup on a domain name (i.e. SLD). */ /** Represents a WHOIS lookup on a domain name (i.e. SLD). */
class DomainLookupCommand extends DomainOrHostLookupCommand<DomainResource> { public class DomainLookupCommand extends DomainOrHostLookupCommand {
DomainLookupCommand(InternetDomainName domainName) { DomainLookupCommand(InternetDomainName domainName) {
this(domainName, null); this(domainName, null);
@ -31,7 +34,10 @@ class DomainLookupCommand extends DomainOrHostLookupCommand<DomainResource> {
} }
@Override @Override
WhoisResponse getSuccessResponse(DomainResource domain, DateTime now) { protected Optional<WhoisResponse> getResponse(InternetDomainName domainName, DateTime now) {
return new DomainWhoisResponse(domain, now); final DomainResource domainResource =
loadByForeignKey(DomainResource.class, domainName.toString(), now);
return Optional.fromNullable(
domainResource == null ? null : new DomainWhoisResponse(domainResource, now));
} }
} }

View file

@ -15,7 +15,6 @@
package google.registry.whois; package google.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.registry.Registries.findTldForName; import static google.registry.model.registry.Registries.findTldForName;
import static google.registry.model.registry.Registries.getTlds; import static google.registry.model.registry.Registries.getTlds;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
@ -23,16 +22,13 @@ import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.net.InternetDomainName; import com.google.common.net.InternetDomainName;
import google.registry.model.EppResource;
import google.registry.util.TypeUtils.TypeInstantiator;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.joda.time.DateTime; import org.joda.time.DateTime;
/** Represents a WHOIS lookup on a domain name (i.e. SLD) or a nameserver. */ /** Represents a WHOIS lookup on a domain name (i.e. SLD) or a nameserver. */
abstract class DomainOrHostLookupCommand<T extends EppResource> implements WhoisCommand { public abstract class DomainOrHostLookupCommand implements WhoisCommand {
@VisibleForTesting @VisibleForTesting final InternetDomainName domainOrHostName;
final InternetDomainName domainOrHostName;
private final String errorPrefix; private final String errorPrefix;
@ -52,17 +48,15 @@ abstract class DomainOrHostLookupCommand<T extends EppResource> implements Whois
} }
// Google Policy: Do not return records under TLDs for which we're not authoritative. // Google Policy: Do not return records under TLDs for which we're not authoritative.
if (tld.isPresent() && getTlds().contains(tld.get().toString())) { if (tld.isPresent() && getTlds().contains(tld.get().toString())) {
T domainOrHost = loadByForeignKey( final Optional<WhoisResponse> response = getResponse(domainOrHostName, now);
new TypeInstantiator<T>(getClass()){}.getExactType(), if (response.isPresent()) {
domainOrHostName.toString(), return response.get();
now);
if (domainOrHost != null) {
return getSuccessResponse(domainOrHost, now);
} }
} }
throw new WhoisException(now, SC_NOT_FOUND, errorPrefix + " not found."); throw new WhoisException(now, SC_NOT_FOUND, errorPrefix + " not found.");
} }
/** Renders a response record, provided its successfully retrieved datastore entity. */ /** Renders a response record, provided its successfully retrieved datastore entity. */
abstract WhoisResponse getSuccessResponse(T domainOrHost, DateTime now); protected abstract Optional<WhoisResponse> getResponse(
InternetDomainName domainName, DateTime now);
} }

View file

@ -14,13 +14,16 @@
package google.registry.whois; package google.registry.whois;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import com.google.common.base.Optional;
import com.google.common.net.InternetDomainName; import com.google.common.net.InternetDomainName;
import google.registry.model.host.HostResource; import google.registry.model.host.HostResource;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import org.joda.time.DateTime; import org.joda.time.DateTime;
/** Represents a WHOIS lookup on a nameserver based on its hostname. */ /** Represents a WHOIS lookup on a nameserver based on its hostname. */
final class NameserverLookupByHostCommand extends DomainOrHostLookupCommand<HostResource> { public class NameserverLookupByHostCommand extends DomainOrHostLookupCommand {
NameserverLookupByHostCommand(InternetDomainName hostName) { NameserverLookupByHostCommand(InternetDomainName hostName) {
this(hostName, null); this(hostName, null);
@ -31,7 +34,10 @@ final class NameserverLookupByHostCommand extends DomainOrHostLookupCommand<Host
} }
@Override @Override
WhoisResponse getSuccessResponse(HostResource host, DateTime now) { protected Optional<WhoisResponse> getResponse(InternetDomainName hostName, DateTime now) {
return new NameserverWhoisResponse(host, now); final HostResource hostResource =
loadByForeignKey(HostResource.class, hostName.toString(), now);
return Optional.fromNullable(
hostResource == null ? null : new NameserverWhoisResponse(hostResource, now));
} }
} }

View file

@ -26,18 +26,23 @@ public final class Whois {
private final Clock clock; private final Clock clock;
private final String disclaimer; private final String disclaimer;
private final WhoisCommandFactory commandFactory;
@Inject @Inject
public Whois(Clock clock, @Config("whoisDisclaimer") String disclaimer) { public Whois(
Clock clock,
@Config("whoisDisclaimer") String disclaimer,
@Config("whoisCommandFactory") WhoisCommandFactory commandFactory) {
this.clock = clock; this.clock = clock;
this.disclaimer = disclaimer; this.disclaimer = disclaimer;
this.commandFactory = commandFactory;
} }
/** Performs a WHOIS lookup on a plaintext query string. */ /** Performs a WHOIS lookup on a plaintext query string. */
public String lookup(String query, boolean preferUnicode) { public String lookup(String query, boolean preferUnicode) {
DateTime now = clock.nowUtc(); DateTime now = clock.nowUtc();
try { try {
return new WhoisReader(new StringReader(query), now) return new WhoisReader(new StringReader(query), commandFactory, now)
.readCommand() .readCommand()
.executeQuery(now) .executeQuery(now)
.getPlainTextOutput(preferUnicode, disclaimer); .getPlainTextOutput(preferUnicode, disclaimer);

View file

@ -17,7 +17,7 @@ package google.registry.whois;
import org.joda.time.DateTime; import org.joda.time.DateTime;
/** Represents a WHOIS command request from a client. */ /** Represents a WHOIS command request from a client. */
interface WhoisCommand { public interface WhoisCommand {
/** /**
* Executes a WHOIS query and returns the resultant data. * Executes a WHOIS query and returns the resultant data.

View file

@ -0,0 +1,74 @@
// Copyright 2016 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 com.google.common.net.InternetDomainName;
import google.registry.config.RegistryConfig.ConfigModule;
import java.net.InetAddress;
import javax.annotation.Nullable;
/**
* A class used to configure WHOIS commands.
*
* <p>To add custom commands, extend this class, then configure it in
* {@link ConfigModule#provideWhoisCommandFactoryClass()}.
*/
public class WhoisCommandFactory {
/** Returns a new {@link WhoisCommand} to perform a domain lookup on the specified domain name. */
public final WhoisCommand domainLookup(InternetDomainName domainName) {
return domainLookup(domainName, null);
}
/**
* Returns a new {@link WhoisCommand} to perform a domain lookup on the specified domain name in
* the specified TLD.
*/
public WhoisCommand domainLookup(
InternetDomainName domainName, @Nullable InternetDomainName tld) {
return new DomainLookupCommand(domainName, tld);
}
/**
* Returns a new {@link WhoisCommand} to perform a nameserver lookup on the specified IP address.
*/
public WhoisCommand nameserverLookupByIp(InetAddress inetAddress) {
return new NameserverLookupByIpCommand(inetAddress);
}
/**
* Returns a new {@link WhoisCommand} to perform a nameserver lookup on the specified host name.
*/
public final WhoisCommand nameserverLookupByHost(InternetDomainName hostName) {
return nameserverLookupByHost(hostName, null);
}
/**
* Returns a new {@link WhoisCommand} to perform a nameserver lookup on the specified host name in
* the specified TLD.
*/
public WhoisCommand nameserverLookupByHost(
InternetDomainName hostName, @Nullable InternetDomainName tld) {
return new NameserverLookupByHostCommand(hostName, tld);
}
/**
* Returns a new {@link WhoisCommand} to perform a registrar lookup on the specified registrar
* name.
*/
public WhoisCommand registrarLookup(String registrar) {
return new RegistrarLookupCommand(registrar);
}
}

View file

@ -132,6 +132,7 @@ public final class WhoisHttpServer implements Runnable {
@Inject Response response; @Inject Response response;
@Inject @Config("whoisDisclaimer") String disclaimer; @Inject @Config("whoisDisclaimer") String disclaimer;
@Inject @Config("whoisHttpExpires") Duration expires; @Inject @Config("whoisHttpExpires") Duration expires;
@Inject @Config("whoisCommandFactory") WhoisCommandFactory commandFactory;
@Inject @RequestPath String requestPath; @Inject @RequestPath String requestPath;
@Inject WhoisHttpServer() {} @Inject WhoisHttpServer() {}
@ -144,7 +145,8 @@ public final class WhoisHttpServer implements Runnable {
String command = decode(JOINER.join(SLASHER.split(path.substring(PATH.length())))) + "\r\n"; String command = decode(JOINER.join(SLASHER.split(path.substring(PATH.length())))) + "\r\n";
Reader reader = new StringReader(command); Reader reader = new StringReader(command);
DateTime now = clock.nowUtc(); DateTime now = clock.nowUtc();
sendResponse(SC_OK, new WhoisReader(reader, now).readCommand().executeQuery(now)); sendResponse(
SC_OK, new WhoisReader(reader, commandFactory, now).readCommand().executeQuery(now));
} catch (WhoisException e) { } catch (WhoisException e) {
sendResponse(e.getStatus(), e); sendResponse(e.getStatus(), e);
} catch (IOException e) { } catch (IOException e) {

View file

@ -14,8 +14,12 @@
package google.registry.whois; package google.registry.whois;
import static google.registry.util.TypeUtils.getClassFromString;
import static google.registry.util.TypeUtils.instantiate;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import java.io.IOException; import java.io.IOException;
import java.io.Reader; import java.io.Reader;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -42,4 +46,11 @@ public final class WhoisModule {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
@Provides
@Config("whoisCommandFactory")
static WhoisCommandFactory provideWhoisCommandFactory(
@Config("whoisCommandFactoryClass") String factoryClass) {
return instantiate(getClassFromString(factoryClass, WhoisCommandFactory.class));
}
} }

View file

@ -70,10 +70,12 @@ class WhoisReader {
private final Reader reader; private final Reader reader;
private final DateTime now; private final DateTime now;
private final WhoisCommandFactory commandFactory;
/** Creates a new WhoisReader that extracts its command from the specified Reader. */ /** Creates a new WhoisReader that extracts its command from the specified Reader. */
WhoisReader(Reader reader, DateTime now) { WhoisReader(Reader reader, WhoisCommandFactory commandFactory, DateTime now) {
this.reader = checkNotNull(reader, "reader"); this.reader = checkNotNull(reader, "reader");
this.commandFactory = checkNotNull(commandFactory, "commandFactory");
this.now = checkNotNull(now, "now"); this.now = checkNotNull(now, "now");
} }
@ -111,7 +113,7 @@ class WhoisReader {
// Try to parse the argument as a domain name. // Try to parse the argument as a domain name.
try { try {
return new DomainLookupCommand(InternetDomainName.from( return commandFactory.domainLookup(InternetDomainName.from(
canonicalizeDomainName(tokens.get(1)))); canonicalizeDomainName(tokens.get(1))));
} catch (IllegalArgumentException iae) { } catch (IllegalArgumentException iae) {
// If we can't interpret the argument as a host name, then return an error. // If we can't interpret the argument as a host name, then return an error.
@ -129,14 +131,14 @@ class WhoisReader {
// Try to parse the argument as an IP address. // Try to parse the argument as an IP address.
try { try {
return new NameserverLookupByIpCommand(InetAddresses.forString(tokens.get(1))); return commandFactory.nameserverLookupByIp(InetAddresses.forString(tokens.get(1)));
} catch (IllegalArgumentException iae) { } catch (IllegalArgumentException iae) {
// Silently ignore this exception. // Silently ignore this exception.
} }
// Try to parse the argument as a host name. // Try to parse the argument as a host name.
try { try {
return new NameserverLookupByHostCommand(InternetDomainName.from( return commandFactory.nameserverLookupByHost(InternetDomainName.from(
canonicalizeDomainName(tokens.get(1)))); canonicalizeDomainName(tokens.get(1))));
} catch (IllegalArgumentException iae) { } catch (IllegalArgumentException iae) {
// Silently ignore this exception. // Silently ignore this exception.
@ -153,14 +155,14 @@ class WhoisReader {
throw new WhoisException(now, SC_BAD_REQUEST, String.format( throw new WhoisException(now, SC_BAD_REQUEST, String.format(
"Too few arguments to '%s' command.", REGISTRAR_LOOKUP_COMMAND)); "Too few arguments to '%s' command.", REGISTRAR_LOOKUP_COMMAND));
} }
return new RegistrarLookupCommand(Joiner.on(' ').join(tokens.subList(1, tokens.size()))); return commandFactory.registrarLookup(Joiner.on(' ').join(tokens.subList(1, tokens.size())));
} }
// If we have a single token, then try to interpret that in various ways. // If we have a single token, then try to interpret that in various ways.
if (tokens.size() == 1) { if (tokens.size() == 1) {
// Try to parse it as an IP address. If successful, then this is a lookup on a nameserver. // Try to parse it as an IP address. If successful, then this is a lookup on a nameserver.
try { try {
return new NameserverLookupByIpCommand(InetAddresses.forString(arg1)); return commandFactory.nameserverLookupByIp(InetAddresses.forString(arg1));
} catch (IllegalArgumentException iae) { } catch (IllegalArgumentException iae) {
// Silently ignore this exception. // Silently ignore this exception.
} }
@ -180,11 +182,11 @@ class WhoisReader {
// If the target is exactly one level above the TLD, then this is an second level domain // If the target is exactly one level above the TLD, then this is an second level domain
// (SLD) and we should do a domain lookup on it. // (SLD) and we should do a domain lookup on it.
if (targetName.parent().equals(tld.get())) { if (targetName.parent().equals(tld.get())) {
return new DomainLookupCommand(targetName, tld.get()); return commandFactory.domainLookup(targetName, tld.get());
} }
// The target is more than one level above the TLD, so we'll assume it's a nameserver. // The target is more than one level above the TLD, so we'll assume it's a nameserver.
return new NameserverLookupByHostCommand(targetName, tld.get()); return commandFactory.nameserverLookupByHost(targetName, tld.get());
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
// Silently ignore this exception. // Silently ignore this exception.
} }
@ -194,7 +196,7 @@ class WhoisReader {
// The only case left is that there are multiple tokens with no particular command given. We'll // 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. // assume this is a registrar lookup, since there's really nothing else it could be.
return new RegistrarLookupCommand(Joiner.on(' ').join(tokens)); return commandFactory.registrarLookup(Joiner.on(' ').join(tokens));
} }
/** Returns an ArrayList containing the contents of the String array minus any empty strings. */ /** Returns an ArrayList containing the contents of the String array minus any empty strings. */

View file

@ -58,9 +58,8 @@ public class WhoisServer implements Runnable {
@Inject Clock clock; @Inject Clock clock;
@Inject Reader input; @Inject Reader input;
@Inject Response response; @Inject Response response;
@Inject @Inject @Config("whoisCommandFactory") WhoisCommandFactory commandFactory;
@Config("whoisDisclaimer") @Inject @Config("whoisDisclaimer") String disclaimer;
String disclaimer;
@Inject WhoisServer() {} @Inject WhoisServer() {}
@Override @Override
@ -69,7 +68,7 @@ public class WhoisServer implements Runnable {
DateTime now = clock.nowUtc(); DateTime now = clock.nowUtc();
try { try {
responseText = responseText =
new WhoisReader(input, now) new WhoisReader(input, commandFactory, now)
.readCommand() .readCommand()
.executeQuery(now) .executeQuery(now)
.getPlainTextOutput(PREFER_UNICODE, disclaimer); .getPlainTextOutput(PREFER_UNICODE, disclaimer);

View file

@ -71,6 +71,7 @@ public class WhoisHttpServerTest {
result.requestPath = WhoisHttpServer.PATH + pathInfo; result.requestPath = WhoisHttpServer.PATH + pathInfo;
result.response = response; result.response = response;
result.disclaimer = "Doodle Disclaimer"; result.disclaimer = "Doodle Disclaimer";
result.commandFactory = new WhoisCommandFactory();
return result; return result;
} }

View file

@ -31,13 +31,9 @@ import org.junit.runners.JUnit4;
@RunWith(JUnit4.class) @RunWith(JUnit4.class)
public class WhoisReaderTest { public class WhoisReaderTest {
@Rule @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
public final AppEngineRule appEngine = AppEngineRule.builder()
.withDatastore()
.build();
@Rule @Rule public final ExceptionRule thrown = new ExceptionRule();
public final ExceptionRule thrown = new ExceptionRule();
private final FakeClock clock = new FakeClock(); private final FakeClock clock = new FakeClock();
@ -48,7 +44,9 @@ public class WhoisReaderTest {
@SuppressWarnings("unchecked") // XXX: Generic abuse ftw. @SuppressWarnings("unchecked") // XXX: Generic abuse ftw.
<T> T readCommand(String commandStr) throws Exception { <T> T readCommand(String commandStr) throws Exception {
return (T) new WhoisReader(new StringReader(commandStr), clock.nowUtc()).readCommand(); return (T)
new WhoisReader(new StringReader(commandStr), new WhoisCommandFactory(), clock.nowUtc())
.readCommand();
} }
void assertLoadsExampleTld(String commandString) throws Exception { void assertLoadsExampleTld(String commandString) throws Exception {

View file

@ -66,6 +66,7 @@ public class WhoisServerTest {
result.input = new StringReader(input); result.input = new StringReader(input);
result.response = response; result.response = response;
result.disclaimer = "Doodle Disclaimer"; result.disclaimer = "Doodle Disclaimer";
result.commandFactory = new WhoisCommandFactory();
return result; return result;
} }