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";
}
@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 = ""
+ "# 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"

View file

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

View file

@ -14,13 +14,16 @@
package google.registry.whois;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import com.google.common.base.Optional;
import com.google.common.net.InternetDomainName;
import google.registry.model.domain.DomainResource;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/** Represents a WHOIS lookup on a domain name (i.e. SLD). */
class DomainLookupCommand extends DomainOrHostLookupCommand<DomainResource> {
public class DomainLookupCommand extends DomainOrHostLookupCommand {
DomainLookupCommand(InternetDomainName domainName) {
this(domainName, null);
@ -31,7 +34,10 @@ class DomainLookupCommand extends DomainOrHostLookupCommand<DomainResource> {
}
@Override
WhoisResponse getSuccessResponse(DomainResource domain, DateTime now) {
return new DomainWhoisResponse(domain, now);
protected Optional<WhoisResponse> getResponse(InternetDomainName domainName, DateTime 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;
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.getTlds;
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.base.Optional;
import com.google.common.net.InternetDomainName;
import google.registry.model.EppResource;
import google.registry.util.TypeUtils.TypeInstantiator;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/** 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
final InternetDomainName domainOrHostName;
@VisibleForTesting final InternetDomainName domainOrHostName;
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.
if (tld.isPresent() && getTlds().contains(tld.get().toString())) {
T domainOrHost = loadByForeignKey(
new TypeInstantiator<T>(getClass()){}.getExactType(),
domainOrHostName.toString(),
now);
if (domainOrHost != null) {
return getSuccessResponse(domainOrHost, now);
final Optional<WhoisResponse> response = getResponse(domainOrHostName, now);
if (response.isPresent()) {
return response.get();
}
}
throw new WhoisException(now, SC_NOT_FOUND, errorPrefix + " not found.");
}
/** 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;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import com.google.common.base.Optional;
import com.google.common.net.InternetDomainName;
import google.registry.model.host.HostResource;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/** 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) {
this(hostName, null);
@ -31,7 +34,10 @@ final class NameserverLookupByHostCommand extends DomainOrHostLookupCommand<Host
}
@Override
WhoisResponse getSuccessResponse(HostResource host, DateTime now) {
return new NameserverWhoisResponse(host, now);
protected Optional<WhoisResponse> getResponse(InternetDomainName hostName, DateTime 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 String disclaimer;
private final WhoisCommandFactory commandFactory;
@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.disclaimer = disclaimer;
this.commandFactory = commandFactory;
}
/** Performs a WHOIS lookup on a plaintext query string. */
public String lookup(String query, boolean preferUnicode) {
DateTime now = clock.nowUtc();
try {
return new WhoisReader(new StringReader(query), now)
return new WhoisReader(new StringReader(query), commandFactory, now)
.readCommand()
.executeQuery(now)
.getPlainTextOutput(preferUnicode, disclaimer);

View file

@ -17,7 +17,7 @@ package google.registry.whois;
import org.joda.time.DateTime;
/** Represents a WHOIS command request from a client. */
interface WhoisCommand {
public interface WhoisCommand {
/**
* 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 @Config("whoisDisclaimer") String disclaimer;
@Inject @Config("whoisHttpExpires") Duration expires;
@Inject @Config("whoisCommandFactory") WhoisCommandFactory commandFactory;
@Inject @RequestPath String requestPath;
@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";
Reader reader = new StringReader(command);
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) {
sendResponse(e.getStatus(), e);
} catch (IOException e) {

View file

@ -14,8 +14,12 @@
package google.registry.whois;
import static google.registry.util.TypeUtils.getClassFromString;
import static google.registry.util.TypeUtils.instantiate;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
import java.io.IOException;
import java.io.Reader;
import javax.servlet.http.HttpServletRequest;
@ -42,4 +46,11 @@ public final class WhoisModule {
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 DateTime now;
private final WhoisCommandFactory commandFactory;
/** 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.commandFactory = checkNotNull(commandFactory, "commandFactory");
this.now = checkNotNull(now, "now");
}
@ -111,7 +113,7 @@ class WhoisReader {
// Try to parse the argument as a domain name.
try {
return new DomainLookupCommand(InternetDomainName.from(
return commandFactory.domainLookup(InternetDomainName.from(
canonicalizeDomainName(tokens.get(1))));
} catch (IllegalArgumentException iae) {
// 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 {
return new NameserverLookupByIpCommand(InetAddresses.forString(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 {
return new NameserverLookupByHostCommand(InternetDomainName.from(
return commandFactory.nameserverLookupByHost(InternetDomainName.from(
canonicalizeDomainName(tokens.get(1))));
} catch (IllegalArgumentException iae) {
// Silently ignore this exception.
@ -153,14 +155,14 @@ class WhoisReader {
throw new WhoisException(now, SC_BAD_REQUEST, String.format(
"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 (tokens.size() == 1) {
// Try to parse it as an IP address. If successful, then this is a lookup on a nameserver.
try {
return new NameserverLookupByIpCommand(InetAddresses.forString(arg1));
return commandFactory.nameserverLookupByIp(InetAddresses.forString(arg1));
} catch (IllegalArgumentException iae) {
// 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
// (SLD) and we should do a domain lookup on it.
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.
return new NameserverLookupByHostCommand(targetName, tld.get());
return commandFactory.nameserverLookupByHost(targetName, tld.get());
} catch (IllegalArgumentException e) {
// 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
// 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. */

View file

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

View file

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

View file

@ -31,13 +31,9 @@ import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class WhoisReaderTest {
@Rule
public final AppEngineRule appEngine = AppEngineRule.builder()
.withDatastore()
.build();
@Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build();
@Rule
public final ExceptionRule thrown = new ExceptionRule();
@Rule public final ExceptionRule thrown = new ExceptionRule();
private final FakeClock clock = new FakeClock();
@ -48,7 +44,9 @@ public class WhoisReaderTest {
@SuppressWarnings("unchecked") // XXX: Generic abuse ftw.
<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 {

View file

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