1
0
mirror of https://github.com/google/nomulus synced 2026-02-06 04:51:10 +00:00

Remove WHOIS classes and configuration (#2859)

This is steps one and two of b/454947209

We already haven't been serving WHOIS for a while, so there's no point
in keeping the old code around. This can simplify some code paths in the
future (like, certain foreign-key-loads that are only used in WHOIS
queries).
This commit is contained in:
gbrodman
2025-10-27 14:57:25 -04:00
committed by GitHub
parent 19e03dbd2e
commit 6cd351ec7c
105 changed files with 19 additions and 8623 deletions

View File

@@ -55,9 +55,6 @@ def dockerIncompatibleTestPatterns = [
// objects retained by frameworks.
// TODO(weiminyu): identify cause and fix offending tests.
def fragileTestPatterns = [
// Changes cache timeouts and for some reason appears to have contention
// with other tests.
"google/registry/whois/WhoisCommandFactoryTest.*",
// Breaks random other tests when running with standardTests.
"google/registry/bsa/UploadBsaUnavailableDomainsActionTest.*",
// Currently changes a global configuration parameter that for some reason

View File

@@ -976,17 +976,6 @@ public final class RegistryConfig {
return config.misc.transientFailureRetries;
}
/**
* Amount of time public HTTP proxies are permitted to cache our WHOIS responses.
*
* @see google.registry.whois.WhoisHttpAction
*/
@Provides
@Config("whoisHttpExpires")
public static Duration provideWhoisHttpExpires() {
return Duration.standardDays(1);
}
/**
* Maximum number of results to return for an RDAP search query
*
@@ -998,39 +987,6 @@ public final class RegistryConfig {
return 100;
}
/**
* Redaction text for email address in WHOIS
*
* @see google.registry.whois.WhoisResponse
*/
@Provides
@Config("whoisRedactedEmailText")
public static String provideWhoisRedactedEmailText(RegistryConfigSettings config) {
return config.registryPolicy.whoisRedactedEmailText;
}
/**
* Disclaimer displayed at the end of WHOIS query results.
*
* @see google.registry.whois.WhoisResponse
*/
@Provides
@Config("whoisDisclaimer")
public static String provideWhoisDisclaimer(RegistryConfigSettings config) {
return config.registryPolicy.whoisDisclaimer;
}
/**
* Message template for whois response when queried domain is blocked by BSA.
*
* @see google.registry.whois.WhoisResponse
*/
@Provides
@Config("domainBlockedByBsaTemplate")
public static String provideDomainBlockedByBsaTemplate(RegistryConfigSettings config) {
return config.registryPolicy.domainBlockedByBsaTemplate;
}
/**
* Maximum QPS for the Google Cloud Monitoring V3 (aka Stackdriver) API. The QPS limit can be
* adjusted by contacting Cloud Support.
@@ -1105,12 +1061,6 @@ public final class RegistryConfig {
return config.registryPolicy.customLogicFactoryClass;
}
@Provides
@Config("whoisCommandFactoryClass")
public static String provideWhoisCommandFactoryClass(RegistryConfigSettings config) {
return config.registryPolicy.whoisCommandFactoryClass;
}
@Provides
@Config("dnsCountQueryCoordinatorClass")
public static String dnsCountQueryCoordinatorClass(RegistryConfigSettings config) {

View File

@@ -90,7 +90,6 @@ public class RegistryConfigSettings {
public String contactAndHostRoidSuffix;
public String productName;
public String customLogicFactoryClass;
public String whoisCommandFactoryClass;
public String dnsCountQueryCoordinatorClass;
public int contactAutomaticTransferDays;
public String greetingServerId;
@@ -102,9 +101,6 @@ public class RegistryConfigSettings {
public String registryAdminClientId;
public String premiumTermsExportDisclaimer;
public String reservedTermsExportDisclaimer;
public String whoisRedactedEmailText;
public String whoisDisclaimer;
public String domainBlockedByBsaTemplate;
public String rdapTos;
public String rdapTosStaticUrl;
public String registryName;

View File

@@ -65,10 +65,6 @@ registryPolicy:
# See flows/custom/CustomLogicFactory.java
customLogicFactoryClass: google.registry.flows.custom.CustomLogicFactory
# WHOIS command factory fully-qualified class name.
# See whois/WhoisCommandFactory.java
whoisCommandFactoryClass: google.registry.whois.WhoisCommandFactory
# Custom logic class for handling DNS query count reporting for ICANN.
# See reporting/icann/DnsCountQueryCoordinator.java
dnsCountQueryCoordinatorClass: google.registry.reporting.icann.DummyDnsCountQueryCoordinator
@@ -114,31 +110,6 @@ registryPolicy:
to publish. This list is subject to change. The most up-to-date source
is always the registry itself, by sending domain check EPP commands.
# Redaction text for email address in WHOIS
whoisRedactedEmailText: |
Please query the WHOIS server of the owning registrar identified in this
output for information on how to contact the Registrant, Admin, or Tech
contact of the queried domain name.
# Disclaimer at the top of WHOIS results.
whoisDisclaimer: |
WHOIS information is provided by the registry solely for query-based,
informational purposes. Any information provided is "as is" without any
guarantee of accuracy. You may not use such information to (a) allow,
enable, or otherwise support the transmission of mass unsolicited,
commercial advertising or solicitations; (b) enable high volume, automated,
electronic processes that access the registry's systems or any
ICANN-Accredited Registrar, except as reasonably necessary to register
domain names or modify existing registrations; or (c) engage in or support
unlawful behavior. We reserve the right to restrict or deny your access to
the WHOIS database, and may modify these terms at any time.
# BSA blocked domain name template.
domainBlockedByBsaTemplate: |
Domain Name: %s
>>> This name is not available for registration.
>>> This name has been blocked by a GlobalBlock service.
# RDAP Terms of Service text displayed at the /rdap/help/tos endpoint.
rdapTos: >
By querying our Domain Database as part of the RDAP pilot program (RDAP

View File

@@ -57,11 +57,6 @@
<!-- Verbatim JavaScript sources (only visible to admins for debugging). -->
<url-pattern>/assets/sources/*</url-pattern>
<!-- TODO(b/26776367): Move these files to /assets/sources. -->
<url-pattern>/assets/js/registrar_bin.js.map</url-pattern>
<url-pattern>/assets/js/registrar_dbg.js</url-pattern>
<url-pattern>/assets/css/registrar_dbg.css</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>

View File

@@ -130,7 +130,7 @@ public final class EppResourceUtils {
* past.
*
* <p>Do not call this cached version for anything that needs transactional consistency. It should
* only be used when it's OK if the data is potentially being out of date, e.g. WHOIS.
* only be used when it's OK if the data is potentially being out of date, e.g. RDAP.
*
* @param clazz the resource type to load
* @param foreignKey id to match
@@ -181,7 +181,7 @@ public final class EppResourceUtils {
return Optional.empty();
}
// When setting status values based on a time, choose the greater of "now" and the resource's
// UpdateAutoTimestamp. For non-mutating uses (info, whois, etc.), this is equivalent to rolling
// UpdateAutoTimestamp. For non-mutating uses (info, RDAP, etc.), this is equivalent to rolling
// "now" forward to at least the last update on the resource, so that a read right after a write
// doesn't appear stale. For mutating flows, if we had to roll now forward then the flow will
// fail when it tries to save anything, since "now" is needed to be > the last update time for

View File

@@ -492,7 +492,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
return LIVE_STATES.contains(state);
}
/** Returns {@code true} if registrar should be visible in WHOIS results. */
/** Returns {@code true} if registrar should be visible in RDAP results. */
public boolean isLiveAndPubliclyVisible() {
return LIVE_STATES.contains(state) && PUBLICLY_VISIBLE_TYPES.contains(type);
}

View File

@@ -131,9 +131,6 @@ import google.registry.ui.server.console.domains.ConsoleBulkDomainAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.RdapRegistrarFieldsAction;
import google.registry.ui.server.console.settings.SecurityAction;
import google.registry.whois.WhoisAction;
import google.registry.whois.WhoisHttpAction;
import google.registry.whois.WhoisModule;
/** Dagger component with per-request lifetime. */
@RequestScope
@@ -161,8 +158,7 @@ import google.registry.whois.WhoisModule;
Spec11Module.class,
TmchModule.class,
ToolsServerModule.class,
WhiteboxModule.class,
WhoisModule.class,
WhiteboxModule.class
})
interface RequestComponent {
FlowComponent.Builder flowComponentBuilder();
@@ -343,10 +339,6 @@ interface RequestComponent {
VerifyOteAction verifyOteAction();
WhoisAction whoisAction();
WhoisHttpAction whoisHttpAction();
WipeOutContactHistoryPiiAction wipeOutContactHistoryPiiAction();
@Subcomponent.Builder

View File

@@ -36,9 +36,6 @@ import google.registry.rdap.RdapNameserverSearchAction;
import google.registry.request.RequestComponentBuilder;
import google.registry.request.RequestModule;
import google.registry.request.RequestScope;
import google.registry.whois.WhoisAction;
import google.registry.whois.WhoisHttpAction;
import google.registry.whois.WhoisModule;
/** Dagger component with per-request lifetime for "pubapi" App Engine module. */
@RequestScope
@@ -49,8 +46,7 @@ import google.registry.whois.WhoisModule;
EppTlsModule.class,
RdapModule.class,
RequestModule.class,
WhiteboxModule.class,
WhoisModule.class,
WhiteboxModule.class
})
public interface PubApiRequestComponent {
CheckApiAction checkApiAction();
@@ -67,9 +63,6 @@ public interface PubApiRequestComponent {
ReadinessProbeActionPubApi readinessProbeActionPubApi();
WhoisHttpAction whoisHttpAction();
WhoisAction whoisAction();
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder<PubApiRequestComponent> {
@Override public abstract Builder requestModule(RequestModule requestModule);

View File

@@ -67,15 +67,13 @@ class RequestMetrics {
private static String truncatePath(String path) {
// We want to bucket RDAP requests by type to use less metric space,
// e.g. "/rdap/domains" rather than "/rdap/domains/foo.tld"
if (path.startsWith("/rdap")) {
List<String> splitPath = Splitter.on("/").omitEmptyStrings().splitToList(path);
return Streams.stream(Iterables.limit(splitPath, 2))
.collect(Collectors.joining("/", "/", "/"));
// Similarly, we put all web WHOIS requests under the same path because otherwise its
// cardinality is unbound, and it is possible to generate a huge amount of metrics with all
// the different paths.
} else if (path.startsWith("/whois")) {
// Though we don't serve WHOIS requests anymore, bucket any non-served WHOIS requests together
// to avoid an unbound cardinality
return "/whois";
}
return path;

View File

@@ -23,7 +23,7 @@ public enum Auth {
/**
* Allows anyone to access.
*
* <p>This is used for public HTML endpoints like RDAP, the check API, and web WHOIS.
* <p>This is used for public HTML endpoints like RDAP and the check API.
*/
AUTH_PUBLIC(AuthLevel.NONE, UserPolicy.PUBLIC),

View File

@@ -122,7 +122,6 @@ public final class RegistryTool {
.put("validate_escrow_deposit", ValidateEscrowDepositCommand.class)
.put("validate_login_credentials", ValidateLoginCredentialsCommand.class)
.put("verify_ote", VerifyOteCommand.class)
.put("whois_query", WhoisQueryCommand.class)
.build();
public static void main(String[] args) throws Exception {

View File

@@ -37,7 +37,6 @@ import google.registry.request.Modules.GsonModule;
import google.registry.request.Modules.UrlConnectionServiceModule;
import google.registry.tools.AuthModule.LocalCredentialModule;
import google.registry.util.UtilsModule;
import google.registry.whois.NonCachingWhoisModule;
import jakarta.inject.Singleton;
import javax.annotation.Nullable;
@@ -67,8 +66,7 @@ import javax.annotation.Nullable;
RequestFactoryModule.class,
SecretManagerModule.class,
UrlConnectionServiceModule.class,
UtilsModule.class,
NonCachingWhoisModule.class
UtilsModule.class
})
interface RegistryToolComponent {
void inject(AckPollMessagesCommand command);
@@ -163,8 +161,6 @@ interface RegistryToolComponent {
void inject(ValidateLoginCredentialsCommand command);
void inject(WhoisQueryCommand command);
ServiceConnection serviceConnection();
@LocalCredentialJson

View File

@@ -1,48 +0,0 @@
// 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.tools;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Joiner;
import google.registry.whois.Whois;
import jakarta.inject.Inject;
import java.util.List;
/** Command to execute a WHOIS query. */
@Parameters(separators = " =", commandDescription = "Manually perform a WHOIS query")
final class WhoisQueryCommand implements Command {
@Parameter(
description = "WHOIS query string",
required = true)
private List<String> mainParameters;
@Parameter(
names = "--unicode",
description = "When set, output will be Unicode")
private boolean unicode;
@Parameter(names = "--full_output", description = "When set, the full output will be displayed")
private boolean fullOutput;
@Inject
Whois whois;
@Override
public void run() {
System.out.println(whois.lookup(Joiner.on(' ').join(mainParameters), unicode, fullOutput));
}
}

View File

@@ -1,371 +0,0 @@
// 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.ui.server;
import static com.google.common.collect.Range.atLeast;
import static com.google.common.collect.Range.atMost;
import static com.google.common.collect.Range.closed;
import static google.registry.util.DomainNameUtils.canonicalizeHostname;
import com.google.common.base.Ascii;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InternetDomainName;
import com.google.re2j.Pattern;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.ui.forms.FormField;
import google.registry.ui.forms.FormFieldException;
import google.registry.ui.forms.FormFields;
import google.registry.util.CidrAddressBlock;
import google.registry.util.X509Utils;
import java.security.cert.CertificateParsingException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/** Form fields for validating input for the {@code Registrar} class. */
public final class RegistrarFormFields {
public static final Pattern BASE64_PATTERN = Pattern.compile("[+/a-zA-Z0-9]*");
public static final Pattern ASCII_PATTERN = Pattern.compile("[[:ascii:]]*");
public static final String ASCII_ERROR = "Please only use ASCII-US characters.";
public static final FormField<String, String> NAME_FIELD =
FormFields.NAME.asBuilderNamed("registrarName")
.required()
.build();
public static final FormField<String, DateTime> LAST_UPDATE_TIME =
FormFields.LABEL
.asBuilderNamed("lastUpdateTime")
.transform(DateTime.class, RegistrarFormFields::parseDateTime)
.required()
.build();
public static final FormField<String, String> EMAIL_ADDRESS_FIELD_REQUIRED =
FormFields.EMAIL.asBuilderNamed("emailAddress")
.matches(ASCII_PATTERN, ASCII_ERROR)
.required()
.build();
public static final FormField<String, String> EMAIL_ADDRESS_FIELD_OPTIONAL =
FormFields.EMAIL.asBuilderNamed("emailAddress")
.matches(ASCII_PATTERN, ASCII_ERROR)
.build();
public static final FormField<String, String> ICANN_REFERRAL_EMAIL_FIELD =
FormFields.EMAIL.asBuilderNamed("icannReferralEmail")
.matches(ASCII_PATTERN, ASCII_ERROR)
.required()
.build();
public static final FormField<String, String> PHONE_NUMBER_FIELD =
FormFields.PHONE_NUMBER.asBuilder()
.matches(ASCII_PATTERN, ASCII_ERROR)
.build();
public static final FormField<String, String> FAX_NUMBER_FIELD =
FormFields.PHONE_NUMBER.asBuilderNamed("faxNumber")
.matches(ASCII_PATTERN, ASCII_ERROR)
.build();
public static final FormField<List<String>, Set<String>> ALLOWED_TLDS_FIELD =
FormFields.LABEL.asBuilderNamed("allowedTlds")
.asSet()
.build();
public static final FormField<String, String> WHOIS_SERVER_FIELD =
FormFields.LABEL.asBuilderNamed("whoisServer")
.transform(RegistrarFormFields::parseHostname)
.build();
public static final FormField<String, String> CLIENT_CERTIFICATE_HASH_FIELD =
FormField.named("clientCertificateHash")
.emptyToNull()
.matches(BASE64_PATTERN, "Field must contain a base64 value.")
.range(closed(1, 255))
.build();
private static final FormField<String, String> X509_PEM_CERTIFICATE =
FormField.named("certificate")
.emptyToNull()
.transform(
input -> {
if (input == null) {
return null;
}
try {
X509Utils.loadCertificate(input);
} catch (CertificateParsingException e) {
throw new FormFieldException("Invalid X.509 PEM certificate");
}
return input;
})
.build();
public static final FormField<String, String> CLIENT_CERTIFICATE_FIELD =
X509_PEM_CERTIFICATE.asBuilderNamed("clientCertificate").build();
public static final FormField<String, String> FAILOVER_CLIENT_CERTIFICATE_FIELD =
X509_PEM_CERTIFICATE.asBuilderNamed("failoverClientCertificate").build();
public static final FormField<Long, Long> BILLING_IDENTIFIER_FIELD =
FormField.named("billingIdentifier", Long.class)
.range(atLeast(1))
.build();
public static final FormField<Long, Long> IANA_IDENTIFIER_FIELD =
FormField.named("ianaIdentifier", Long.class)
.range(atLeast(1))
.build();
public static final FormField<String, String> URL_FIELD =
FormFields.MIN_TOKEN.asBuilderNamed("url")
.build();
public static final FormField<List<String>, List<CidrAddressBlock>> IP_ADDRESS_ALLOW_LIST_FIELD =
FormField.named("ipAddressAllowList")
.emptyToNull()
.transform(CidrAddressBlock.class, RegistrarFormFields::parseCidr)
.asList()
.build();
public static final FormField<String, String> PASSWORD_FIELD =
FormFields.PASSWORD.asBuilderNamed("password")
.build();
public static final FormField<String, String> PHONE_PASSCODE_FIELD =
FormField.named("phonePasscode")
.emptyToNull()
.matches(Registrar.PHONE_PASSCODE_PATTERN)
.build();
public static final FormField<String, String> CONTACT_NAME_FIELD =
FormFields.NAME.asBuilderNamed("name")
.required()
.build();
public static final FormField<String, String> CONTACT_EMAIL_ADDRESS_FIELD =
FormFields.EMAIL.asBuilderNamed("emailAddress")
.required()
.build();
public static final FormField<String, String> REGISTRY_LOCK_EMAIL_ADDRESS_FIELD =
FormFields.EMAIL.asBuilderNamed("registryLockEmailAddress").build();
public static final FormField<Boolean, Boolean> CONTACT_VISIBLE_IN_WHOIS_AS_ADMIN_FIELD =
FormField.named("visibleInWhoisAsAdmin", Boolean.class)
.build();
public static final FormField<Boolean, Boolean> CONTACT_VISIBLE_IN_WHOIS_AS_TECH_FIELD =
FormField.named("visibleInWhoisAsTech", Boolean.class)
.build();
public static final FormField<Boolean, Boolean>
PHONE_AND_EMAIL_VISIBLE_IN_DOMAIN_WHOIS_AS_ABUSE_FIELD =
FormField.named("visibleInDomainWhoisAsAbuse", Boolean.class).build();
public static final FormField<String, String> CONTACT_PHONE_NUMBER_FIELD =
FormFields.PHONE_NUMBER.asBuilder().build();
public static final FormField<String, String> CONTACT_FAX_NUMBER_FIELD =
FormFields.PHONE_NUMBER.asBuilderNamed("faxNumber").build();
public static final FormField<String, Set<RegistrarPoc.Type>> CONTACT_TYPES =
FormField.named("types")
.uppercased()
.asEnum(RegistrarPoc.Type.class)
.asSet(Splitter.on(',').omitEmptyStrings().trimResults())
.build();
public static final FormField<List<Map<String, ?>>, List<Map<String, ?>>> CONTACTS_AS_MAPS =
FormField.mapNamed("contacts").asList().build();
public static final FormField<List<String>, List<String>> I18N_STREET_FIELD =
FormFields.XS_NORMALIZED_STRING.asBuilderNamed("street")
.range(closed(1, 255))
.matches(ASCII_PATTERN, ASCII_ERROR)
.asList()
.range(closed(1, 3))
.required()
.build();
public static final FormField<List<String>, List<String>> L10N_STREET_FIELD =
FormFields.XS_NORMALIZED_STRING.asBuilderNamed("street")
.range(closed(1, 255))
.asList()
.range(closed(1, 3))
.required()
.build();
public static final FormField<String, String> I18N_CITY_FIELD =
FormFields.NAME.asBuilderNamed("city")
.matches(ASCII_PATTERN, ASCII_ERROR)
.required()
.build();
public static final FormField<String, String> L10N_CITY_FIELD =
FormFields.NAME.asBuilderNamed("city")
.required()
.build();
public static final FormField<String, String> I18N_STATE_FIELD =
FormFields.XS_NORMALIZED_STRING.asBuilderNamed("state")
.range(atMost(255))
.matches(ASCII_PATTERN, ASCII_ERROR)
.build();
public static final FormField<String, String> L10N_STATE_FIELD =
FormFields.XS_NORMALIZED_STRING.asBuilderNamed("state")
.range(atMost(255))
.build();
public static final FormField<String, String> I18N_ZIP_FIELD =
FormFields.XS_TOKEN.asBuilderNamed("zip")
.range(atMost(16))
.matches(ASCII_PATTERN, ASCII_ERROR)
.build();
public static final FormField<String, String> L10N_ZIP_FIELD =
FormFields.XS_TOKEN.asBuilderNamed("zip")
.range(atMost(16))
.build();
public static final FormField<String, String> COUNTRY_CODE_FIELD =
FormFields.COUNTRY_CODE.asBuilder()
.required()
.build();
public static final FormField<Map<String, ?>, RegistrarAddress> L10N_ADDRESS_FIELD =
FormField.mapNamed("localizedAddress")
.transform(
RegistrarAddress.class,
args ->
toNewAddress(
args, L10N_STREET_FIELD, L10N_CITY_FIELD, L10N_STATE_FIELD, L10N_ZIP_FIELD))
.build();
@Nullable
private static RegistrarAddress toNewAddress(
@Nullable Map<String, ?> args,
final FormField<List<String>, List<String>> streetField,
final FormField<String, String> cityField,
final FormField<String, String> stateField,
final FormField<String, String> zipField) {
if (args == null) {
return null;
}
RegistrarAddress.Builder builder = new RegistrarAddress.Builder();
String countryCode = COUNTRY_CODE_FIELD.extractUntyped(args).get();
builder.setCountryCode(countryCode);
streetField
.extractUntyped(args)
.ifPresent(streets -> builder.setStreet(ImmutableList.copyOf(streets)));
cityField.extractUntyped(args).ifPresent(builder::setCity);
Optional<String> stateFieldValue = stateField.extractUntyped(args);
if (stateFieldValue.isPresent()) {
String state = stateFieldValue.get();
if ("US".equals(countryCode)) {
state = Ascii.toUpperCase(state);
if (!StateCode.US_MAP.containsKey(state)) {
throw new FormFieldException(stateField, "Unknown US state code.");
}
}
builder.setState(state);
}
zipField.extractUntyped(args).ifPresent(builder::setZip);
return builder.build();
}
private static CidrAddressBlock parseCidr(String input) {
try {
return input != null ? CidrAddressBlock.create(input) : null;
} catch (IllegalArgumentException e) {
throw new FormFieldException("Not a valid CIDR notation IP-address block.", e);
}
}
@Nullable
private static String parseHostname(@Nullable String input) {
if (input == null) {
return null;
}
if (!InternetDomainName.isValid(input)) {
throw new FormFieldException("Not a valid hostname.");
}
return canonicalizeHostname(input);
}
@Nullable
public static DateTime parseDateTime(@Nullable String input) {
if (input == null) {
return null;
}
try {
return DateTime.parse(input);
} catch (IllegalArgumentException e) {
throw new FormFieldException("Not a valid ISO date-time string.", e);
}
}
public static ImmutableList<RegistrarPoc.Builder> getRegistrarContactBuilders(
ImmutableSet<RegistrarPoc> existingContacts, @Nullable Map<String, ?> args) {
if (args == null) {
return ImmutableList.of();
}
Optional<List<Map<String, ?>>> contactsAsMaps = CONTACTS_AS_MAPS.extractUntyped(args);
if (contactsAsMaps.isEmpty()) {
return ImmutableList.of();
}
ImmutableList.Builder<RegistrarPoc.Builder> result = new ImmutableList.Builder<>();
for (Map<String, ?> contactAsMap : contactsAsMaps.get()) {
String emailAddress =
CONTACT_EMAIL_ADDRESS_FIELD
.extractUntyped(contactAsMap)
.orElseThrow(
() -> new IllegalArgumentException("Contacts from UI must have email addresses"));
// Start with a new builder if the contact didn't previously exist
RegistrarPoc.Builder contactBuilder =
existingContacts.stream()
.filter(rc -> rc.getEmailAddress().equals(emailAddress))
.findFirst()
.map(RegistrarPoc::asBuilder)
.orElse(new RegistrarPoc.Builder());
applyRegistrarContactArgs(contactBuilder, contactAsMap);
result.add(contactBuilder);
}
return result.build();
}
private static void applyRegistrarContactArgs(RegistrarPoc.Builder builder, Map<String, ?> args) {
builder.setName(CONTACT_NAME_FIELD.extractUntyped(args).orElse(null));
builder.setEmailAddress(CONTACT_EMAIL_ADDRESS_FIELD.extractUntyped(args).orElse(null));
builder.setVisibleInWhoisAsAdmin(
CONTACT_VISIBLE_IN_WHOIS_AS_ADMIN_FIELD.extractUntyped(args).orElse(false));
builder.setVisibleInWhoisAsTech(
CONTACT_VISIBLE_IN_WHOIS_AS_TECH_FIELD.extractUntyped(args).orElse(false));
builder.setVisibleInDomainWhoisAsAbuse(
PHONE_AND_EMAIL_VISIBLE_IN_DOMAIN_WHOIS_AS_ABUSE_FIELD.extractUntyped(args).orElse(false));
builder.setPhoneNumber(CONTACT_PHONE_NUMBER_FIELD.extractUntyped(args).orElse(null));
builder.setFaxNumber(CONTACT_FAX_NUMBER_FIELD.extractUntyped(args).orElse(null));
builder.setTypes(CONTACT_TYPES.extractUntyped(args).orElse(ImmutableSet.of()));
}
}

View File

@@ -38,10 +38,10 @@ import java.util.Optional;
import java.util.StringJoiner;
/**
* Console action for editing fields on a registrar that are visible in WHOIS/RDAP.
* Console action for editing fields on a registrar that are visible in RDAP.
*
* <p>This doesn't cover many of the registrar fields but rather only those that are visible in
* WHOIS/RDAP and don't have any other obvious means of edit.
* <p>This doesn't cover many of the registrar fields but rather only those that are visible in RDAP
* and don't have any other obvious means of edit.
*/
@Action(
service = GaeService.DEFAULT,

View File

@@ -1,52 +0,0 @@
// Copyright 2021 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 dagger.Module;
import dagger.Provides;
import google.registry.util.Clock;
import google.registry.whois.WhoisMetrics.WhoisMetric;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.Reader;
/**
* Dagger base module for the whois package.
*
* <p>Provides whois objects common to both the normal ({@link WhoisModule}) and non-caching ({@link
* NonCachingWhoisModule}) implementations.
*/
@Module
class BaseWhoisModule {
@Provides
@SuppressWarnings("CloseableProvides")
static Reader provideHttpInputReader(HttpServletRequest req) {
try {
return req.getReader();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Provides a {@link WhoisMetrics.WhoisMetric.Builder} with the startTimestamp already
* initialized.
*/
@Provides
static WhoisMetric.Builder provideEppMetricBuilder(Clock clock) {
return WhoisMetric.builderForRequest(clock);
}
}

View File

@@ -1,121 +0,0 @@
// 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 google.registry.flows.domain.DomainFlowUtils.isBlockedByBsa;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.model.tld.Tlds.findTldForName;
import static google.registry.model.tld.Tlds.getTlds;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Verify;
import com.google.common.net.InternetDomainName;
import google.registry.model.domain.Domain;
import google.registry.model.tld.Tld;
import java.util.Optional;
import org.joda.time.DateTime;
/** Represents a WHOIS lookup on a domain name (i.e. SLD). */
public class DomainLookupCommand implements WhoisCommand {
private static final String ERROR_PREFIX = "Domain";
@VisibleForTesting final InternetDomainName domainName;
private final boolean fullOutput;
private final boolean cached;
private final String whoisRedactedEmailText;
private final String domainBlockedByBsaTemplate;
public DomainLookupCommand(
InternetDomainName domainName,
boolean fullOutput,
boolean cached,
String whoisRedactedEmailText,
String domainBlockedByBsaTemplate) {
this.domainName = checkNotNull(domainName, "domainOrHostName");
this.fullOutput = fullOutput;
this.cached = cached;
this.whoisRedactedEmailText = whoisRedactedEmailText;
this.domainBlockedByBsaTemplate = domainBlockedByBsaTemplate;
}
@Override
public final WhoisResponse executeQuery(final DateTime now) throws WhoisException {
Optional<InternetDomainName> tld = findTldForName(domainName);
// Google Registry Policy: Do not return records under TLDs for which we're not
// authoritative.
if (tld.isEmpty() || !getTlds().contains(tld.get().toString())) {
throw new WhoisException(now, SC_NOT_FOUND, ERROR_PREFIX + " not found.");
}
// Include `getResponse` and `isBlockedByBsa` in one transaction to reduce latency.
// Must pass the exceptions outside to throw.
ResponseOrException result =
replicaTm()
.transact(
() -> {
final Optional<WhoisResponse> response = getResponse(domainName, now);
if (response.isPresent()) {
return ResponseOrException.of(response.get());
}
String label = domainName.parts().get(0);
String tldStr = tld.get().toString();
if (isBlockedByBsa(label, Tld.get(tldStr), now)) {
return ResponseOrException.of(
new WhoisException(
now,
SC_NOT_FOUND,
String.format(domainBlockedByBsaTemplate, domainName)));
}
return ResponseOrException.of(
new WhoisException(now, SC_NOT_FOUND, ERROR_PREFIX + " not found."));
});
return result.returnOrThrow();
}
private Optional<WhoisResponse> getResponse(InternetDomainName domainName, DateTime now) {
Optional<Domain> domainResource =
cached
? loadByForeignKeyByCache(Domain.class, domainName.toString(), now)
: loadByForeignKey(Domain.class, domainName.toString(), now);
return domainResource.map(
domain -> new DomainWhoisResponse(domain, fullOutput, whoisRedactedEmailText, now));
}
record ResponseOrException(
Optional<WhoisResponse> whoisResponse, Optional<WhoisException> exception) {
WhoisResponse returnOrThrow() throws WhoisException {
Verify.verify(
whoisResponse().isPresent() || exception().isPresent(),
"Response and exception must not both be missing.");
return whoisResponse().orElseThrow(() -> exception().get());
}
static ResponseOrException of(WhoisResponse response) {
return new ResponseOrException(Optional.of(response), Optional.empty());
}
static ResponseOrException of(WhoisException exception) {
return new ResponseOrException(Optional.empty(), Optional.of(exception));
}
}
}

View File

@@ -1,208 +0,0 @@
// 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.Preconditions.checkState;
import static google.registry.util.CollectionUtils.isNullOrEmpty;
import static google.registry.xml.UtcDateTimeAdapter.getFormattedString;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.model.EppResource;
import google.registry.model.adapters.EnumToAttributeAdapter.EppEnum;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactPhoneNumber;
import google.registry.model.contact.PostalInfo;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.Domain;
import google.registry.model.domain.GracePeriod;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.persistence.VKey;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/** Represents a WHOIS response to a domain query. */
final class DomainWhoisResponse extends WhoisResponseImpl {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Prefix for status value URLs. */
private static final String ICANN_STATUS_URL_PREFIX = "https://icann.org/epp#";
/** Message required to be appended to all domain WHOIS responses. */
private static final String ICANN_AWIP_INFO_MESSAGE =
"For more information on Whois status codes, please visit https://icann.org/epp\r\n";
/** Domain which was the target of this WHOIS command. */
private final Domain domain;
/** Whether the full WHOIS output is to be displayed. */
private final boolean fullOutput;
/** When fullOutput is false, the text to display for the registrant's email fields. */
private final String whoisRedactedEmailText;
/** Creates new WHOIS domain response on the given domain. */
DomainWhoisResponse(
Domain domain, boolean fullOutput, String whoisRedactedEmailText, DateTime timestamp) {
super(timestamp);
this.domain = checkNotNull(domain, "domain");
this.fullOutput = fullOutput;
this.whoisRedactedEmailText = whoisRedactedEmailText;
}
@Override
public WhoisResponseResults getResponse(final boolean preferUnicode, String disclaimer) {
Optional<Registrar> registrarOptional =
Registrar.loadByRegistrarIdCached(domain.getCurrentSponsorRegistrarId());
checkState(
registrarOptional.isPresent(),
"Could not load registrar %s",
domain.getCurrentSponsorRegistrarId());
Registrar registrar = registrarOptional.get();
Optional<RegistrarPoc> abuseContact =
registrar.getPocsFromReplica().stream()
.filter(RegistrarPoc::getVisibleInDomainWhoisAsAbuse)
.findFirst();
return WhoisResponseResults.create(
new DomainEmitter()
.emitField("Domain Name", maybeFormatHostname(domain.getDomainName(), preferUnicode))
.emitField("Registry Domain ID", domain.getRepoId())
.emitField("Registrar WHOIS Server", registrar.getWhoisServer())
.emitField("Registrar URL", registrar.getUrl())
.emitFieldIfDefined("Updated Date", getFormattedString(domain.getLastEppUpdateTime()))
.emitField("Creation Date", getFormattedString(domain.getCreationTime()))
.emitField(
"Registry Expiry Date", getFormattedString(domain.getRegistrationExpirationTime()))
.emitField("Registrar", registrar.getRegistrarName())
.emitField("Registrar IANA ID", Objects.toString(registrar.getIanaIdentifier(), ""))
// Email address is a required field for registrar contacts. Therefore as long as there
// is an abuse contact, we can get an email address from it.
.emitField(
"Registrar Abuse Contact Email",
abuseContact.map(RegistrarPoc::getEmailAddress).orElse(""))
.emitField(
"Registrar Abuse Contact Phone",
abuseContact.map(RegistrarPoc::getPhoneNumber).orElse(""))
.emitStatusValues(domain.getStatusValues(), domain.getGracePeriods())
// TODO(mcilwain): Investigate if the WHOIS spec requires us to always output REDACTED
// text in WHOIS even if the contact is not present, and if so, do so.
.emitContact("Registrant", domain.getRegistrant(), preferUnicode)
.emitContact("Admin", getContactReference(Type.ADMIN), preferUnicode)
.emitContact("Tech", getContactReference(Type.TECH), preferUnicode)
.emitContact("Billing", getContactReference(Type.BILLING), preferUnicode)
.emitSet(
"Name Server",
domain.loadNameserverHostNames(),
hostName -> maybeFormatHostname(hostName, preferUnicode))
.emitField(
"DNSSEC", isNullOrEmpty(domain.getDsData()) ? "unsigned" : "signedDelegation")
.emitWicfLink()
.emitLastUpdated(getTimestamp())
.emitAwipMessage()
.emitFooter(disclaimer)
.toString(),
1);
}
/** Returns the contact of the given type. */
private Optional<VKey<Contact>> getContactReference(Type type) {
Optional<DesignatedContact> contactOfType =
domain.getContacts().stream().filter(d -> d.getType() == type).findFirst();
return contactOfType.map(DesignatedContact::getContactKey);
}
/** Output emitter with logic for domains. */
class DomainEmitter extends Emitter<DomainEmitter> {
DomainEmitter emitPhone(
String contactType, String title, @Nullable ContactPhoneNumber phoneNumber) {
if (phoneNumber == null) {
return this;
}
return emitFieldIfDefined(
ImmutableList.of(contactType, title), phoneNumber.getPhoneNumber(), fullOutput)
.emitFieldIfDefined(
ImmutableList.of(contactType, title, "Ext"), phoneNumber.getExtension(), fullOutput);
}
/** Emit the contact entry of the given type. */
DomainEmitter emitContact(
String contactType, Optional<VKey<Contact>> contact, boolean preferUnicode) {
if (contact.isEmpty()) {
return this;
}
// If we refer to a contact that doesn't exist, that's a bug. It means referential integrity
// has somehow been broken. We skip the rest of this contact, but log it to hopefully bring it
// someone's attention.
Contact contact1 = EppResource.loadByCache(contact.get());
if (contact1 == null) {
logger.atSevere().log(
"(BUG) Broken reference found from domain %s to contact %s.",
domain.getDomainName(), contact);
return this;
}
PostalInfo postalInfo =
chooseByUnicodePreference(
preferUnicode,
contact1.getLocalizedPostalInfo(),
contact1.getInternationalizedPostalInfo());
// ICANN Consistent Labeling & Display policy requires that this be the ROID.
emitField(ImmutableList.of("Registry", contactType, "ID"), contact1.getRepoId(), fullOutput);
if (postalInfo != null) {
emitFieldIfDefined(ImmutableList.of(contactType, "Name"), postalInfo.getName(), fullOutput);
emitFieldIfDefined(
ImmutableList.of(contactType, "Organization"),
postalInfo.getOrg(),
fullOutput || contactType.equals("Registrant"));
emitAddress(contactType, postalInfo.getAddress(), fullOutput);
}
emitPhone(contactType, "Phone", contact1.getVoiceNumber());
emitPhone(contactType, "Fax", contact1.getFaxNumber());
String emailFieldContent = fullOutput ? contact1.getEmailAddress() : whoisRedactedEmailText;
emitField(ImmutableList.of(contactType, "Email"), emailFieldContent);
return this;
}
/** Emits status values and grace periods as a set, in the AWIP format. */
DomainEmitter emitStatusValues(
Set<StatusValue> statusValues, Set<GracePeriod> gracePeriods) {
ImmutableSet.Builder<EppEnum> combinedStatuses = new ImmutableSet.Builder<>();
combinedStatuses.addAll(statusValues);
for (GracePeriod gracePeriod : gracePeriods) {
combinedStatuses.add(gracePeriod.getType());
}
return emitSet(
"Domain Status",
combinedStatuses.build(),
status -> {
String xmlName = status.getXmlName();
return String.format("%s %s%s", xmlName, ICANN_STATUS_URL_PREFIX, xmlName);
});
}
/** Emits the message that AWIP requires accompany all domain WHOIS responses. */
DomainEmitter emitAwipMessage() {
return emitRawLine(ICANN_AWIP_INFO_MESSAGE);
}
}
}

View File

@@ -1,64 +0,0 @@
// 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 google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCache;
import static google.registry.model.tld.Tlds.findTldForName;
import static google.registry.model.tld.Tlds.getTlds;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.net.InternetDomainName;
import google.registry.model.host.Host;
import java.util.Optional;
import org.joda.time.DateTime;
/** Represents a WHOIS lookup on a nameserver based on its hostname. */
public class NameserverLookupByHostCommand implements WhoisCommand {
private static final String ERROR_PREFIX = "Nameserver";
@VisibleForTesting final InternetDomainName hostName;
boolean cached;
NameserverLookupByHostCommand(InternetDomainName hostName, boolean cached) {
this.hostName = checkNotNull(hostName, "hostName");
this.cached = cached;
}
@Override
public final WhoisResponse executeQuery(final DateTime now) throws WhoisException {
Optional<InternetDomainName> tld = findTldForName(hostName);
// Google Registry Policy: Do not return records under TLDs for which we're not authoritative.
if (tld.isPresent() && getTlds().contains(tld.get().toString())) {
final Optional<WhoisResponse> response = getResponse(hostName, now);
if (response.isPresent()) {
return response.get();
}
}
throw new WhoisException(now, SC_NOT_FOUND, ERROR_PREFIX + " not found.");
}
private Optional<WhoisResponse> getResponse(InternetDomainName hostName, DateTime now) {
Optional<Host> host =
cached
? loadByForeignKeyByCache(Host.class, hostName.toString(), now)
: loadByForeignKey(Host.class, hostName.toString(), now);
return host.map(h -> new NameserverWhoisResponse(h, now));
}
}

View File

@@ -1,84 +0,0 @@
// 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.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Streams;
import com.google.common.net.InetAddresses;
import com.google.common.net.InternetDomainName;
import google.registry.model.host.Host;
import google.registry.model.tld.Tlds;
import java.net.InetAddress;
import org.joda.time.DateTime;
/**
* Represents a WHOIS lookup for a nameserver based on its IP.
*
* <p>Both IPv4 and IPv6 addresses are supported. Unlike other WHOIS commands, this is an eventually
* consistent query.
*
* <p><b>Note:</b> There may be multiple nameservers with the same IP.
*/
final class NameserverLookupByIpCommand implements WhoisCommand {
@VisibleForTesting
final InetAddress ipAddress;
NameserverLookupByIpCommand(InetAddress ipAddress) {
this.ipAddress = checkNotNull(ipAddress, "ipAddress");
}
@Override
@SuppressWarnings("unchecked")
public WhoisResponse executeQuery(DateTime now) throws WhoisException {
Iterable<Host> hostsFromDb;
hostsFromDb =
replicaTm()
.transact(
() ->
// We cannot query @Convert-ed fields in HQL, so we must use native Postgres.
replicaTm()
.getEntityManager()
/*
* Using array_operator <@ (contained-by) with gin index on inet_address.
* Without gin index, this is slightly slower than the alternative form of
* ':address = ANY(inet_address)'.
*/
.createNativeQuery(
"SELECT * From \"Host\" WHERE "
+ "ARRAY[ CAST(:address AS TEXT) ] <@ inet_addresses AND "
+ "deletion_time > CAST(:now AS timestamptz)",
Host.class)
.setParameter("address", InetAddresses.toAddrString(ipAddress))
.setParameter("now", now.toString())
.getResultList());
ImmutableList<Host> hosts =
Streams.stream(hostsFromDb)
.filter(
host ->
Tlds.findTldForName(InternetDomainName.from(host.getHostName())).isPresent())
.collect(toImmutableList());
if (hosts.isEmpty()) {
throw new WhoisException(now, SC_NOT_FOUND, "No nameservers found.");
}
return new NameserverWhoisResponse(hosts, now);
}
}

View File

@@ -1,89 +0,0 @@
// 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.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.net.InetAddresses;
import google.registry.model.host.Host;
import google.registry.model.registrar.Registrar;
import java.util.Optional;
import org.joda.time.DateTime;
/** Container for WHOIS responses to a nameserver lookup queries. */
final class NameserverWhoisResponse extends WhoisResponseImpl {
/** Nameserver(s) which were the target of this WHOIS command. */
private final ImmutableList<Host> hosts;
/** Creates new WHOIS nameserver response on the given host. */
NameserverWhoisResponse(Host host, DateTime timestamp) {
this(ImmutableList.of(checkNotNull(host, "host")), timestamp);
}
/** Creates new WHOIS nameserver response on the given list of hosts. */
NameserverWhoisResponse(ImmutableList<Host> hosts, DateTime timestamp) {
super(timestamp);
this.hosts = checkNotNull(hosts, "hosts");
}
@Override
public WhoisResponseResults getResponse(boolean preferUnicode, String disclaimer) {
// If we have subordinate hosts, load their registrar ids in a single transaction up-front.
ImmutableList<Host> subordinateHosts =
hosts.stream().filter(Host::isSubordinate).collect(toImmutableList());
ImmutableMap<Host, String> hostRegistrars =
subordinateHosts.isEmpty()
? ImmutableMap.of()
: replicaTm()
.transact(
() ->
Maps.toMap(
subordinateHosts.iterator(),
host ->
replicaTm()
.loadByKey(host.getSuperordinateDomain())
.cloneProjectedAtTime(getTimestamp())
.getCurrentSponsorRegistrarId()));
BasicEmitter emitter = new BasicEmitter();
for (int i = 0; i < hosts.size(); i++) {
Host host = hosts.get(i);
String registrarId =
host.isSubordinate()
? hostRegistrars.get(host)
: host.getPersistedCurrentSponsorRegistrarId();
Optional<Registrar> registrar = Registrar.loadByRegistrarIdCached(registrarId);
checkState(registrar.isPresent(), "Could not load registrar %s", registrarId);
emitter
.emitField("Server Name", maybeFormatHostname(host.getHostName(), preferUnicode))
.emitSet("IP Address", host.getInetAddresses(), InetAddresses::toAddrString)
.emitField("Registrar", registrar.get().getRegistrarName())
.emitField("Registrar WHOIS Server", registrar.get().getWhoisServer())
.emitField("Registrar URL", registrar.get().getUrl());
if (i < hosts.size() - 1) {
emitter.emitNewline();
}
}
String plaintext = emitter.emitLastUpdated(getTimestamp()).emitFooter(disclaimer).toString();
return WhoisResponseResults.create(plaintext, hosts.size());
}
}

View File

@@ -1,39 +0,0 @@
// Copyright 2021 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 dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
/**
* Whois module for systems that require that we not cache EPP resources (e.g. the nomulus tool).
*
* <h2>Dependencies</h2>
*
* <ul>
* <li>{@link google.registry.request.RequestModule RequestModule}
* </ul>
*
* @see "google.registry.tools.RegistryToolComponent"
*/
@Module
public final class NonCachingWhoisModule extends BaseWhoisModule {
@Provides
@Config("whoisCommandFactory")
static WhoisCommandFactory provideWhoisCommandFactory() {
return WhoisCommandFactory.createNonCached();
}
}

View File

@@ -1,108 +0,0 @@
// 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.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
import static google.registry.util.RegistrarUtils.normalizeRegistrarName;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import google.registry.model.registrar.Registrar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.joda.time.DateTime;
/** Represents a WHOIS lookup for a registrar by its name. */
final class RegistrarLookupCommand implements WhoisCommand {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** True if the command should return cached responses. */
private boolean cached;
/**
* Cache of a map from a stripped-down (letters and digits only) name to the registrar. This map
* includes only active, publicly visible registrars, because the others should be invisible to
* WHOIS.
*/
private static final Supplier<Map<String, Registrar>> REGISTRAR_BY_NORMALIZED_NAME_CACHE =
memoizeWithShortExpiration(RegistrarLookupCommand::loadRegistrarMap);
@VisibleForTesting
final String registrarName;
RegistrarLookupCommand(String registrarName, boolean cached) {
checkArgument(!isNullOrEmpty(registrarName), "registrarName");
this.registrarName = registrarName;
this.cached = cached;
}
static Map<String, Registrar> loadRegistrarMap() {
Map<String, Registrar> map = new HashMap<>();
// Use the normalized registrar name as a key, and ignore inactive and hidden
// registrars.
for (Registrar registrar : Registrar.loadAll()) {
if (!registrar.isLiveAndPubliclyVisible() || registrar.getRegistrarName() == null) {
continue;
}
String normalized = normalizeRegistrarName(registrar.getRegistrarName());
if (map.put(normalized, registrar) != null) {
logger.atWarning().log(
"%s appeared as a normalized registrar name for more than one registrar.", normalized);
}
}
// Use the normalized registrar name without its last word as a key, assuming there are
// multiple words in the name. This allows searches without LLC or INC, etc. Only insert
// if there isn't already a mapping for this string, so that if there's a registrar with
// a
// two word name (Go Daddy) and no business-type suffix and another registrar with just
// that first word as its name (Go), the latter will win.
for (Registrar registrar : ImmutableList.copyOf(map.values())) {
if (registrar.getRegistrarName() == null) {
continue;
}
List<String> words =
Splitter.on(CharMatcher.whitespace()).splitToList(registrar.getRegistrarName());
if (words.size() > 1) {
String normalized =
normalizeRegistrarName(Joiner.on("").join(words.subList(0, words.size() - 1)));
map.putIfAbsent(normalized, registrar);
}
}
return ImmutableMap.copyOf(map);
}
@Override
public WhoisResponse executeQuery(DateTime now) throws WhoisException {
Map<String, Registrar> registrars =
cached ? REGISTRAR_BY_NORMALIZED_NAME_CACHE.get() : loadRegistrarMap();
Registrar registrar = registrars.get(normalizeRegistrarName(registrarName));
// If a registrar is in the cache, we know it must be active and publicly visible.
if (registrar == null) {
throw new WhoisException(now, SC_NOT_FOUND, "No registrar found.");
}
return new RegistrarWhoisResponse(registrar, now);
}
}

View File

@@ -1,96 +0,0 @@
// 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 google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import java.util.Set;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/** Container for WHOIS responses to registrar lookup queries. */
class RegistrarWhoisResponse extends WhoisResponseImpl {
/** Registrar which was the target of this WHOIS command. */
private final Registrar registrar;
/**
* Used in the emitter below to signal either admin or tech
* contacts. NB, this is purposely distinct from the
* RegistrarContact.Type.{ADMIN,TECH} as they don't carry equivalent
* meaning in our system. Sigh.
*/
private enum AdminOrTech { ADMIN, TECH }
/** Creates a new WHOIS registrar response on the given registrar object. */
RegistrarWhoisResponse(Registrar registrar, DateTime timestamp) {
super(timestamp);
this.registrar = checkNotNull(registrar, "registrar");
}
@Override
public WhoisResponseResults getResponse(boolean preferUnicode, String disclaimer) {
Set<RegistrarPoc> contacts = registrar.getPocsFromReplica();
String plaintext =
new RegistrarEmitter()
.emitField("Registrar", registrar.getRegistrarName())
.emitAddress(
null,
chooseByUnicodePreference(
preferUnicode,
registrar.getLocalizedAddress(),
registrar.getInternationalizedAddress()),
true)
.emitPhonesAndEmail(
registrar.getPhoneNumber(), registrar.getFaxNumber(), registrar.getEmailAddress())
.emitField("Registrar WHOIS Server", registrar.getWhoisServer())
.emitField("Registrar URL", registrar.getUrl())
.emitRegistrarPocs("Admin", contacts, AdminOrTech.ADMIN)
.emitRegistrarPocs("Technical", contacts, AdminOrTech.TECH)
.emitLastUpdated(getTimestamp())
.emitFooter(disclaimer)
.toString();
return WhoisResponseResults.create(plaintext, 1);
}
/** An emitter with logic for registrars. */
static class RegistrarEmitter extends Emitter<RegistrarEmitter> {
/** Emits the registrar contact of the given type. */
RegistrarEmitter emitRegistrarPocs(
String contactLabel, Iterable<RegistrarPoc> contacts, AdminOrTech type) {
for (RegistrarPoc contact : contacts) {
if ((type == AdminOrTech.ADMIN && contact.getVisibleInWhoisAsAdmin())
|| (type == AdminOrTech.TECH && contact.getVisibleInWhoisAsTech())) {
emitField(contactLabel + " Contact", contact.getName())
.emitPhonesAndEmail(
contact.getPhoneNumber(),
contact.getFaxNumber(),
contact.getEmailAddress());
}
}
return this;
}
/** Emits the registrar contact of the given type. */
RegistrarEmitter emitPhonesAndEmail(
@Nullable String phone, @Nullable String fax, @Nullable String email) {
return emitField("Phone Number", phone)
.emitField("Fax Number", fax)
.emitField("Email", email);
}
}
}

View File

@@ -1,53 +0,0 @@
// 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 google.registry.config.RegistryConfig.Config;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.io.IOException;
import java.io.StringReader;
import org.joda.time.DateTime;
/** High-level WHOIS API for other packages. */
public final class Whois {
private final Clock clock;
private final String disclaimer;
private final WhoisReader whoisReader;
@Inject
public Whois(Clock clock, @Config("whoisDisclaimer") String disclaimer, WhoisReader whoisReader) {
this.clock = clock;
this.disclaimer = disclaimer;
this.whoisReader = whoisReader;
}
/** Performs a WHOIS lookup on a plaintext query string. */
public String lookup(String query, boolean preferUnicode, boolean fullOutput) {
DateTime now = clock.nowUtc();
try {
return whoisReader
.readCommand(new StringReader(query), fullOutput, now)
.executeQuery(now)
.getResponse(preferUnicode, disclaimer)
.plainTextOutput();
} catch (WhoisException e) {
return e.getResponse(preferUnicode, disclaimer).plainTextOutput();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -1,121 +0,0 @@
// 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 google.registry.request.Action.Method.POST;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.Retrier;
import google.registry.whois.WhoisException.UncheckedWhoisException;
import google.registry.whois.WhoisMetrics.WhoisMetric;
import google.registry.whois.WhoisResponse.WhoisResponseResults;
import jakarta.inject.Inject;
import java.io.Reader;
import org.joda.time.DateTime;
/**
* HTTP request handler for WHOIS protocol requests sent to us by a proxy.
*
* <p>All commands and responses conform to the WHOIS spec as defined in RFC 3912. Commands must be
* sent via an HTTP POST in the request body.
*
* <p>This action is meant to serve as a low level interface for the proxy app which forwards us
* requests received on port 43. However this interface is technically higher level because it sends
* back proper HTTP error codes such as 200, 400, 500, etc. These are discarded by the proxy because
* WHOIS specifies no manner for differentiating successful and erroneous requests.
*
* @see WhoisHttpAction
* @see <a href="http://www.ietf.org/rfc/rfc3912.txt">RFC 3912: WHOIS Protocol Specification</a>
*/
@Action(service = GaeService.PUBAPI, path = "/_dr/whois", method = POST, auth = Auth.AUTH_ADMIN)
public class WhoisAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** WHOIS doesn't define an encoding, nor any way to specify an encoding in the protocol. */
static final MediaType CONTENT_TYPE = MediaType.PLAIN_TEXT_UTF_8;
/**
* As stated above, this is the low level interface intended for port 43, and as such, it
* always prefers ASCII.
*/
static final boolean PREFER_UNICODE = false;
@Inject Clock clock;
@Inject Reader input;
@Inject Response response;
@Inject Retrier retrier;
@Inject WhoisReader whoisReader;
@Inject @Config("whoisDisclaimer") String disclaimer;
@Inject WhoisMetric.Builder metricBuilder;
@Inject WhoisMetrics whoisMetrics;
@Inject
WhoisAction() {}
@Override
public void run() {
String responseText;
final DateTime now = clock.nowUtc();
try {
final WhoisCommand command = whoisReader.readCommand(input, false, now);
metricBuilder.setCommand(command);
WhoisResponseResults results;
try {
results = command.executeQuery(now).getResponse(PREFER_UNICODE, disclaimer);
} catch (WhoisException e) {
throw new UncheckedWhoisException(e);
}
responseText = results.plainTextOutput();
setWhoisMetrics(metricBuilder, results.numResults(), SC_OK);
} catch (UncheckedWhoisException u) {
WhoisException e = (WhoisException) u.getCause();
WhoisResponseResults results = e.getResponse(PREFER_UNICODE, disclaimer);
responseText = results.plainTextOutput();
setWhoisMetrics(metricBuilder, 0, e.getStatus());
} catch (WhoisException e) {
WhoisResponseResults results = e.getResponse(PREFER_UNICODE, disclaimer);
responseText = results.plainTextOutput();
setWhoisMetrics(metricBuilder, 0, e.getStatus());
} catch (Throwable t) {
logger.atSevere().withCause(t).log("WHOIS request crashed.");
responseText = "Internal Server Error";
setWhoisMetrics(metricBuilder, 0, SC_INTERNAL_SERVER_ERROR);
}
// Note that we always return 200 (OK) even if an error was hit. This is because returning an
// non-OK HTTP status code will cause the proxy server to silently close the connection. Since
// WHOIS has no way to return errors, it's better to convert any such errors into strings and
// return them directly.
response.setStatus(SC_OK);
response.setContentType(CONTENT_TYPE);
response.setPayload(responseText);
whoisMetrics.recordWhoisMetric(metricBuilder.build());
}
private static void setWhoisMetrics(
WhoisMetric.Builder metricBuilder, int numResults, int status) {
metricBuilder.setNumResults(numResults);
metricBuilder.setStatus(status);
}
}

View File

@@ -1,29 +0,0 @@
// 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 org.joda.time.DateTime;
/** Represents a WHOIS command request from a client. */
public interface WhoisCommand {
/**
* Executes a WHOIS query and returns the resultant data.
*
* @return An object representing the response to the WHOIS command.
* @throws WhoisException If some error occured while executing the command.
*/
WhoisResponse executeQuery(DateTime now) throws WhoisException;
}

View File

@@ -1,80 +0,0 @@
// 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 com.google.common.net.InternetDomainName;
import google.registry.config.RegistryConfig.ConfigModule;
import java.net.InetAddress;
/**
* 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 {
/** True if the commands should return cached responses. */
private boolean cached = true;
/** Public default constructor, needed so we can construct this from the class name. */
public WhoisCommandFactory() {}
private WhoisCommandFactory(boolean cached) {
this.cached = cached;
}
/** Create a command factory that does not rely on entity caches. */
static WhoisCommandFactory createNonCached() {
return new WhoisCommandFactory(false);
}
/** Create a command factory that may rely on entity caches. */
static WhoisCommandFactory createCached() {
return new WhoisCommandFactory(true);
}
/** Returns a new {@link WhoisCommand} to perform a domain lookup on the specified domain name. */
public WhoisCommand domainLookup(
InternetDomainName domainName,
boolean fullOutput,
String whoisRedactedEmailText,
String domainBlockedByBsaTemplate) {
return new DomainLookupCommand(
domainName, fullOutput, cached, whoisRedactedEmailText, domainBlockedByBsaTemplate);
}
/**
* 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 WhoisCommand nameserverLookupByHost(InternetDomainName hostName) {
return new NameserverLookupByHostCommand(hostName, cached);
}
/**
* Returns a new {@link WhoisCommand} to perform a registrar lookup on the specified registrar
* name.
*/
public WhoisCommand registrarLookup(String registrar) {
return new RegistrarLookupCommand(registrar, cached);
}
}

View File

@@ -1,78 +0,0 @@
// 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.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/** Exception that gets thrown when WHOIS command isn't successful. */
public final class WhoisException extends Exception implements WhoisResponse {
private final DateTime timestamp;
private final int status;
/** @see #WhoisException(DateTime, int, String, Throwable) */
WhoisException(DateTime timestamp, int status, String message) {
this(timestamp, status, message, null);
}
/**
* Construct an exception explaining why a WHOIS request has failed.
*
* @param timestamp should be set to the time at which this request was processed.
* @param status A non-2xx HTTP status code to indicate type of failure.
* @param message is displayed to the user so you should be careful about tainted data.
* @param cause the original exception or {@code null}.
* @throws IllegalArgumentException if {@code !(300 <= status < 700)}
*/
WhoisException(DateTime timestamp, int status, String message, @Nullable Throwable cause) {
super(message, cause);
checkArgument(300 <= status && status < 700,
"WhoisException status must be a non-2xx HTTP status code: %s", status);
this.timestamp = checkNotNull(timestamp, "timestamp");
this.status = status;
}
/** Returns the time at which this WHOIS request was processed. */
@Override
public DateTime getTimestamp() {
return timestamp;
}
/** Returns a non-2xx HTTP status code to differentiate types of failure. */
public int getStatus() {
return status;
}
@Override
public WhoisResponseResults getResponse(boolean preferUnicode, String disclaimer) {
String plaintext = new WhoisResponseImpl.BasicEmitter()
.emitRawLine(getMessage())
.emitLastUpdated(getTimestamp())
.emitFooter(disclaimer)
.toString();
return WhoisResponseResults.create(plaintext, 0);
}
/** Exception that wraps WhoisExceptions returned from Retrier. */
public static final class UncheckedWhoisException extends RuntimeException {
UncheckedWhoisException(WhoisException whoisException) {
super(whoisException);
}
}
}

View File

@@ -1,198 +0,0 @@
// 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.Strings.nullToEmpty;
import static com.google.common.base.Verify.verify;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
import static com.google.common.net.HttpHeaders.CACHE_CONTROL;
import static com.google.common.net.HttpHeaders.EXPIRES;
import static com.google.common.net.HttpHeaders.LAST_MODIFIED;
import static com.google.common.net.HttpHeaders.X_CONTENT_TYPE_OPTIONS;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Throwables;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.RequestPath;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.whois.WhoisMetrics.WhoisMetric;
import google.registry.whois.WhoisResponse.WhoisResponseResults;
import jakarta.inject.Inject;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* Human-Friendly HTTP WHOIS API
*
* <p>This API uses easy to understand paths rather than {@link WhoisAction} which requires a POST
* request containing a WHOIS command. Because the typical WHOIS command is along the lines of
* {@code "domain google.lol"} or the equivalent {@code "google.lol}, this action is just going to
* replace the slashes with spaces and let {@link WhoisReader} figure out what to do.
*
* <p>This action accepts requests from any origin.
*
* <p>You can send AJAX requests to our WHOIS API from your <em>very own</em> website using the
* following embed code:
*
* <pre>{@code
* <input id="query-input" placeholder="Domain, Nameserver, IP, etc." autofocus>
* <button id="search-button">Lookup</button>
* <pre id="whois-results"></pre>
* <script>
* (function() {
* var WHOIS_API_URL = 'https://domain-registry-alpha.appspot.com/whois/';
* function OnKeyPressQueryInput(ev) {
* if (typeof ev == 'undefined' && window.event) {
* ev = window.event;
* }
* if (ev.keyCode == 13) {
* document.getElementById('search-button').click();
* }
* }
* function OnClickSearchButton() {
* var query = document.getElementById('query-input').value;
* var req = new XMLHttpRequest();
* req.onreadystatechange = function() {
* if (req.readyState == 4) {
* var results = document.getElementById('whois-results');
* results.textContent = req.responseText;
* }
* };
* req.open('GET', WHOIS_API_URL + escape(query), true);
* req.send();
* }
* document.getElementById('search-button').onclick = OnClickSearchButton;
* document.getElementById('query-input').onkeypress = OnKeyPressQueryInput;
* })();
* </script>
* }</pre>
*
* @see WhoisAction
*/
@Action(
service = GaeService.PUBAPI,
path = WhoisHttpAction.PATH,
isPrefix = true,
auth = Auth.AUTH_PUBLIC)
public final class WhoisHttpAction implements Runnable {
public static final String PATH = "/whois/";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
* Cross-origin resource sharing (CORS) allowed origins policy.
*
* <p>This field specifies the value of the {@code Access-Control-Allow-Origin} response header.
* Without this header, other domains such as charlestonroadregistry.com would not be able to
* send requests to our WHOIS interface.
*
* <p>Our policy shall be to allow requests from pretty much anywhere using a wildcard policy.
* The reason this is safe is because our WHOIS interface doesn't allow clients to modify data,
* nor does it allow them to fetch user data. Only publicly available information is returned.
*
* @see <a href="http://www.w3.org/TR/cors/#access-control-allow-origin-response-header">
* W3C CORS § 5.1 Access-Control-Allow-Origin Response Header</a>
*/
private static final String CORS_ALLOW_ORIGIN = "*";
/** We're going to let any HTTP proxy in the world cache our responses. */
private static final String CACHE_CONTROL_VALUE = "public";
/** Responses may be cached for up to a day. */
private static final String X_CONTENT_NO_SNIFF = "nosniff";
/** Splitter that turns information on HTTP into a list of tokens. */
private static final Splitter SLASHER = Splitter.on('/').trimResults().omitEmptyStrings();
/** Joiner that turns {@link #SLASHER} tokens into a normal WHOIS query. */
private static final Joiner JOINER = Joiner.on(' ');
@Inject Clock clock;
@Inject Response response;
@Inject @Config("whoisDisclaimer") String disclaimer;
@Inject @Config("whoisHttpExpires") Duration expires;
@Inject WhoisReader whoisReader;
@Inject @RequestPath String requestPath;
@Inject WhoisMetric.Builder metricBuilder;
@Inject WhoisMetrics whoisMetrics;
@Inject
WhoisHttpAction() {}
@Override
public void run() {
verify(requestPath.startsWith(PATH));
String path = nullToEmpty(requestPath);
try {
// Extremely permissive parsing that turns stuff like "/hello/world/" into "hello world".
String commandText =
decode(JOINER.join(SLASHER.split(path.substring(PATH.length())))) + "\r\n";
DateTime now = clock.nowUtc();
WhoisCommand command = whoisReader.readCommand(new StringReader(commandText), false, now);
metricBuilder.setCommand(command);
sendResponse(SC_OK, command.executeQuery(now));
} catch (WhoisException e) {
metricBuilder.setStatus(e.getStatus());
metricBuilder.setNumResults(0);
sendResponse(e.getStatus(), e);
} catch (Throwable e) {
metricBuilder.setStatus(SC_INTERNAL_SERVER_ERROR);
metricBuilder.setNumResults(0);
Throwables.throwIfUnchecked(e);
throw new RuntimeException(e);
} finally {
whoisMetrics.recordWhoisMetric(metricBuilder.build());
}
}
private void sendResponse(int status, WhoisResponse whoisResponse) {
response.setStatus(status);
metricBuilder.setStatus(status);
response.setDateHeader(LAST_MODIFIED, whoisResponse.getTimestamp());
response.setDateHeader(EXPIRES, whoisResponse.getTimestamp().plus(expires));
response.setHeader(CACHE_CONTROL, CACHE_CONTROL_VALUE);
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, CORS_ALLOW_ORIGIN);
response.setHeader(X_CONTENT_TYPE_OPTIONS, X_CONTENT_NO_SNIFF);
response.setContentType(PLAIN_TEXT_UTF_8);
WhoisResponseResults results = whoisResponse.getResponse(true, disclaimer);
metricBuilder.setNumResults(results.numResults());
response.setPayload(results.plainTextOutput());
}
/** Removes {@code %xx} escape codes from request path components. */
private String decode(String pathData)
throws UnsupportedEncodingException, WhoisException {
try {
return URLDecoder.decode(pathData, "UTF-8");
} catch (IllegalArgumentException e) {
logger.atInfo().log("Malformed WHOIS request path: %s (%s)", requestPath, pathData);
throw new WhoisException(clock.nowUtc(), SC_BAD_REQUEST, "Malformed path query.");
}
}
}

View File

@@ -1,147 +0,0 @@
// 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.checkState;
import static com.google.monitoring.metrics.EventMetric.DEFAULT_FITTER;
import com.google.auto.value.AutoBuilder;
import com.google.common.collect.ImmutableSet;
import com.google.monitoring.metrics.EventMetric;
import com.google.monitoring.metrics.IncrementableMetric;
import com.google.monitoring.metrics.LabelDescriptor;
import com.google.monitoring.metrics.MetricRegistryImpl;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* Instrumentation for WHOIS requests.
*
* @see WhoisAction
*/
public class WhoisMetrics {
private static final ImmutableSet<LabelDescriptor> LABEL_DESCRIPTORS =
ImmutableSet.of(
LabelDescriptor.create("command_name", "The name of the WHOIS command."),
LabelDescriptor.create(
"num_results", "The number of results returned by the WHOIS command."),
LabelDescriptor.create("status", "The return status of the WHOIS command."));
private static final IncrementableMetric whoisRequests =
MetricRegistryImpl.getDefault()
.newIncrementableMetric(
"/whois/requests", "Count of WHOIS Requests", "count", LABEL_DESCRIPTORS);
private static final EventMetric processingTime =
MetricRegistryImpl.getDefault()
.newEventMetric(
"/whois/processing_time",
"WHOIS Processing Time",
"milliseconds",
LABEL_DESCRIPTORS,
DEFAULT_FITTER);
@Inject
public WhoisMetrics() {}
/** Records the given {@link WhoisMetric} and its associated processing time. */
public void recordWhoisMetric(WhoisMetric metric) {
whoisRequests.increment(
metric.commandName().orElse(""),
Integer.toString(metric.numResults()),
Integer.toString(metric.status()));
processingTime.record(
metric.endTimestamp().getMillis() - metric.startTimestamp().getMillis(),
metric.commandName().orElse(""),
Integer.toString(metric.numResults()),
Integer.toString(metric.status()));
}
/** A value class for recording attributes of a WHOIS metric. */
public record WhoisMetric(
Optional<String> commandName,
int numResults,
int status,
DateTime startTimestamp,
DateTime endTimestamp) {
/**
* Create a {@link WhoisMetric.Builder} for a request context, with the start and end timestamps
* taken from the given clock.
*
* <p>The start timestamp is recorded now, and the end timestamp at {@code build()}.
*/
public static WhoisMetric.Builder builderForRequest(Clock clock) {
return WhoisMetric.builder().setStartTimestamp(clock.nowUtc()).setClock(clock);
}
/** Create a {@link WhoisMetric.Builder}. */
public static Builder builder() {
return new AutoBuilder_WhoisMetrics_WhoisMetric_Builder();
}
/** A builder to create instances of {@link WhoisMetric}. */
@AutoBuilder
public abstract static class Builder {
boolean wasBuilt = false;
/** Builder-only clock to support automatic recording of endTimestamp on {@link #build()}. */
private Clock clock = null;
public Builder setCommand(WhoisCommand command) {
// All WHOIS command class names share the "Command" suffix, so strip it out in order to
// have shorter labels.
return setCommandName(command.getClass().getSimpleName().replaceFirst("Command$", ""));
}
public abstract Builder setCommandName(String commandName);
public abstract Builder setNumResults(int numResults);
public abstract Builder setStatus(int status);
abstract Builder setStartTimestamp(DateTime startTimestamp);
abstract Builder setEndTimestamp(DateTime endTimestamp);
Builder setClock(Clock clock) {
this.clock = clock;
return this;
}
/**
* Build an instance of {@link WhoisMetric} using this builder.
*
* <p>If a clock was provided with {@code setClock()}, the end timestamp will be set to the
* current timestamp of the clock; otherwise end timestamp must have been previously set.
*/
public WhoisMetric build() {
checkState(
!wasBuilt, "WhoisMetric was already built; building it again is most likely an error");
if (clock != null) {
setEndTimestamp(clock.nowUtc());
}
wasBuilt = true;
return autoBuild();
}
abstract WhoisMetric autoBuild();
}
}
}

View File

@@ -1,43 +0,0 @@
// 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 google.registry.util.TypeUtils.getClassFromString;
import static google.registry.util.TypeUtils.instantiate;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig.Config;
/**
* Dagger module for the whois package.
*
* <h2>Dependencies</h2>
*
* <ul>
* <li>{@link google.registry.request.RequestModule RequestModule}
* </ul>
*
* @see "google.registry.module.frontend.FrontendComponent"
*/
@Module
public final class WhoisModule extends BaseWhoisModule {
@Provides
@Config("whoisCommandFactory")
static WhoisCommandFactory provideWhoisCommandFactory(
@Config("whoisCommandFactoryClass") String factoryClass) {
return instantiate(getClassFromString(factoryClass, WhoisCommandFactory.class));
}
}

View File

@@ -1,241 +0,0 @@
// 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.tld.Tlds.findTldForName;
import static google.registry.util.DomainNameUtils.canonicalizeHostname;
import static jakarta.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 jakarta.inject.Inject;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
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:
*
* <dl>
* <dt>domain &lt;FQDN&gt;<dd>
* Looks up the domain record for the fully qualified domain name.
* <dt>nameserver &lt;FQDN&gt;<dd>
* Looks up the nameserver record for the fully qualified domain name.
* <dt>nameserver &lt;IP&gt;<dd>
* Looks up the nameserver record at the given IP address.
* <dt>registrar &lt;IANA ID&gt;<dd>
* Looks up the registrar record with the given IANA ID.
* <dt>registrar &lt;NAME&gt;<dd>
* Looks up the registrar record with the given name.
* <dt>&lt;IP&gt;<dd>
* Looks up the nameserver record with the given IP address.
* <dt>&lt;FQDN&gt;<dd>
* Looks up the nameserver or domain record for the fully qualified domain name.
* <dt>&lt;IANA ID&gt;<dd>
* Looks up the registrar record with the given IANA ID.
* </dl>
*
* @see <a href="http://tools.ietf.org/html/rfc3912">RFC 3912</a>
* @see <a href="http://www.iana.org/assignments/registrar-ids">IANA Registrar IDs</a>
*/
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;
private final String whoisRedactedEmailText;
private final String domainBlockedByBsaTemplate;
/** Creates a new WhoisReader that extracts its command from the specified Reader. */
@Inject
WhoisReader(
@Config("whoisCommandFactory") WhoisCommandFactory commandFactory,
@Config("whoisRedactedEmailText") String whoisRedactedEmailText,
@Config("domainBlockedByBsaTemplate") String domainBlockedByBsaTemplate) {
this.commandFactory = commandFactory;
this.whoisRedactedEmailText = whoisRedactedEmailText;
this.domainBlockedByBsaTemplate = domainBlockedByBsaTemplate;
}
/**
* 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<String> 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(canonicalizeHostname(tokens.get(1))),
fullOutput,
whoisRedactedEmailText,
domainBlockedByBsaTemplate);
} 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(canonicalizeHostname(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(canonicalizeHostname(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<InternetDomainName> tld = findTldForName(targetName);
if (tld.isEmpty()) {
// 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 commandFactory.registrarLookup(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, whoisRedactedEmailText, domainBlockedByBsaTemplate);
}
// 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<String> filterEmptyStrings(String[] strings) {
List<String> list = new ArrayList<>(strings.length);
for (String str : strings) {
if (!isNullOrEmpty(str)) {
list.add(str);
}
}
return list;
}
}

View File

@@ -1,44 +0,0 @@
// 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 org.joda.time.DateTime;
/** Representation of a WHOIS query response. */
public interface WhoisResponse {
/**
* Returns the WHOIS response.
*
* @param preferUnicode if {@code false} will cause the output to be converted to ASCII whenever
* possible; for example, converting IDN hostname labels to punycode. However certain things
* (like a domain registrant name with accent marks) will be returned "as is". If the WHOIS
* client has told us they're able to receive UTF-8 (such as with HTTP) then this field should
* be set to {@code true}.
* @param disclaimer text to show at bottom of output
*/
WhoisResponseResults getResponse(boolean preferUnicode, String disclaimer);
/** Returns the time at which this response was created. */
DateTime getTimestamp();
/** A wrapper class for the plaintext response of a WHOIS command and its number of results. */
record WhoisResponseResults(String plainTextOutput, int numResults) {
static WhoisResponseResults create(String plainTextOutput, int numResults) {
return new WhoisResponseResults(plainTextOutput, numResults);
}
}
}

View File

@@ -1,245 +0,0 @@
// 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 com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.base.Joiner;
import google.registry.model.eppcommon.Address;
import google.registry.util.Idn;
import google.registry.xml.UtcDateTimeAdapter;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
/** Base class for responses to WHOIS queries. */
abstract class WhoisResponseImpl implements WhoisResponse {
/** Field name for ICANN problem reporting URL appended to all WHOIS responses. */
private static final String ICANN_REPORTING_URL_FIELD =
"URL of the ICANN Whois Inaccuracy Complaint Form";
/** ICANN problem reporting URL appended to all WHOIS responses. */
private static final String ICANN_REPORTING_URL = "https://www.icann.org/wicf/";
/** Text to display when the field is redacted for privacy. */
static final String REDACT_TEXT = "REDACTED FOR PRIVACY";
/** The time at which this response was created. */
private final DateTime timestamp;
WhoisResponseImpl(DateTime timestamp) {
this.timestamp = checkNotNull(timestamp, "timestamp");
}
@Override
public DateTime getTimestamp() {
return timestamp;
}
/**
* Translates a hostname to its unicode representation if desired.
*
* @param hostname is assumed to be in its canonical ASCII form from the database.
*/
static String maybeFormatHostname(String hostname, boolean preferUnicode) {
return preferUnicode ? Idn.toUnicode(hostname) : hostname;
}
static <T> T chooseByUnicodePreference(
boolean preferUnicode, @Nullable T localized, @Nullable T internationalized) {
if (preferUnicode) {
return Optional.ofNullable(localized).orElse(internationalized);
} else {
return Optional.ofNullable(internationalized).orElse(localized);
}
}
/** Writer for outputting data in the WHOIS format. */
abstract static class Emitter<E extends Emitter<E>> {
private final StringBuilder stringBuilder = new StringBuilder();
@SuppressWarnings("unchecked")
private E thisCastToDerived() {
return (E) this;
}
E emitNewline() {
stringBuilder.append("\r\n");
return thisCastToDerived();
}
/**
* Helper method that loops over a set of values and calls {@link #emitField}. This method will
* turn each value into a string using the provided callback and then sort those strings so the
* textual output is deterministic (which is important for unit tests). The ideal solution would
* be to use {@link java.util.SortedSet} but that would require reworking the models.
*/
<T> E emitSet(String title, Set<T> values, Function<T, String> transform) {
return emitList(title, values.stream().map(transform).sorted().collect(toImmutableList()));
}
/**
* Helper method that loops over a list of values and calls {@link #emitField}.
*
* <p>This method redacts the output unless {@code fullOutput} is {@code true}.
*/
E emitList(String title, Iterable<String> values, boolean fullOutput) {
for (String value : values) {
emitField(title, value, fullOutput);
}
return thisCastToDerived();
}
/** Helper method that loops over a list of values and calls {@link #emitField}. */
E emitList(String title, Iterable<String> values) {
return emitList(title, values, true);
}
/**
* Emit the field name and value followed by a newline, but only if a value exists.
*
* <p>This method redacts the output unless {@code fullOutput} is {@code true}.
*/
E emitFieldIfDefined(String name, @Nullable String value, boolean fullOutput) {
if (isNullOrEmpty(value)) {
return thisCastToDerived();
}
stringBuilder.append(cleanse(name)).append(':');
stringBuilder.append(' ').append(fullOutput ? cleanse(value) : REDACT_TEXT);
return emitNewline();
}
/** Emit the field name and value followed by a newline, but only if a value exists. */
E emitFieldIfDefined(String name, @Nullable String value) {
return emitFieldIfDefined(name, value, true);
}
/**
* Emit a multi-part field name and value followed by a newline, but only if a value exists.
*
* <p>This method redacts the output unless {@code fullOutput} is {@code true}.
*/
E emitFieldIfDefined(List<String> nameParts, String value, boolean fullOutput) {
if (isNullOrEmpty(value)) {
return thisCastToDerived();
}
return emitField(nameParts, value, fullOutput);
}
/** Emit a multi-part field name and value followed by a newline, but only if a value exists. */
E emitFieldIfDefined(List<String> nameParts, String value) {
return emitFieldIfDefined(nameParts, value, true);
}
/**
* Emit the field name and value followed by a newline. /*
*
* <p>This method redacts the output unless {@code fullOutput} is {@code true}.
*/
E emitField(String name, @Nullable String value, boolean fullOutput) {
stringBuilder.append(cleanse(name)).append(':');
if (!isNullOrEmpty(value)) {
stringBuilder.append(' ').append(fullOutput ? cleanse(value) : REDACT_TEXT);
}
return emitNewline();
}
/** Emit the field name and value followed by a newline. */
E emitField(String name, @Nullable String value) {
return emitField(name, value, true);
}
/**
* Emit a multi-part field name and value followed by a newline.
*
* <p>This method redacts the output unless {@code fullOutput} is {@code true}.
*/
E emitField(List<String> nameParts, String value, boolean fullOutput) {
return emitField(Joiner.on(' ').join(nameParts), value, fullOutput);
}
/** Emit a multi-part field name and value followed by a newline. */
E emitField(List<String> nameParts, String value) {
return emitField(nameParts, value, true);
}
/** Emit a contact address. */
E emitAddress(@Nullable String prefix, @Nullable Address address, boolean fullOutput) {
prefix = isNullOrEmpty(prefix) ? "" : prefix + " ";
if (address != null) {
emitList(prefix + "Street", address.getStreet(), fullOutput);
emitField(prefix + "City", address.getCity(), fullOutput);
emitField(
prefix + "State/Province",
address.getState(),
(fullOutput || prefix.equals("Registrant ")));
emitField(prefix + "Postal Code", address.getZip(), fullOutput);
emitField(
prefix + "Country",
address.getCountryCode(),
fullOutput || prefix.equals("Registrant "));
}
return thisCastToDerived();
}
/** Emit Whois Inaccuracy Complaint Form link. Only used for domain queries. */
E emitWicfLink() {
emitField(ICANN_REPORTING_URL_FIELD, ICANN_REPORTING_URL);
return thisCastToDerived();
}
/** Returns raw text that should be appended to the end of ALL WHOIS responses. */
E emitLastUpdated(DateTime timestamp) {
// We are assuming that our WHOIS database is always completely up to date, since it's
// querying the live backend database.
stringBuilder
.append(">>> Last update of WHOIS database: ")
.append(UtcDateTimeAdapter.getFormattedString(timestamp))
.append(" <<<\r\n\r\n");
return thisCastToDerived();
}
/** Returns raw text that should be appended to the end of ALL WHOIS responses. */
E emitFooter(String disclaimer) {
stringBuilder.append(disclaimer.replaceAll("\r?\n", "\r\n").trim()).append("\r\n");
return thisCastToDerived();
}
/** Emits a string directly, followed by a newline. */
protected E emitRawLine(String string) {
stringBuilder.append(string);
return emitNewline();
}
/** Remove ASCII control characters like {@code \n} which could be used to forge output. */
private String cleanse(String value) {
return value.replaceAll("[\\x00-\\x1f]", " ");
}
@Override
public String toString() {
return stringBuilder.toString();
}
}
/** An emitter that needs no special logic. */
static class BasicEmitter extends Emitter<BasicEmitter> {}
}

View File

@@ -1,16 +0,0 @@
// 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.
@javax.annotation.ParametersAreNonnullByDefault
package google.registry.whois;

View File

@@ -47,14 +47,12 @@ public final class RegistryTestServer {
public static final ImmutableList<Route> ROUTES =
ImmutableList.of(
// Frontend Services
route("/whois/*", FrontendServlet.class),
route("/rdap/*", FrontendServlet.class),
route("/check", FrontendServlet.class),
route("/console-api/*", FrontendTestServlet.class),
// Proxy Services
route("/_dr/epp", FrontendServlet.class),
route("/_dr/whois", FrontendServlet.class),
// Registry Data Escrow (RDE)
route("/_dr/cron/rdeCreate", BackendServlet.class),

View File

@@ -1,68 +0,0 @@
// 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.ui.server;
import static com.google.common.truth.Truth.assertThat;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.testing.CertificateSamples;
import google.registry.ui.forms.FormFieldException;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link RegistrarFormFields}. */
class RegistrarFormFieldsTest {
@Test
void testValidCertificate_doesntThrowError() {
assertThat(RegistrarFormFields.CLIENT_CERTIFICATE_FIELD.convert(CertificateSamples.SAMPLE_CERT))
.hasValue(CertificateSamples.SAMPLE_CERT);
}
@Test
void testBadCertificate_throwsFfe() {
FormFieldException thrown =
assertThrows(
FormFieldException.class,
() -> RegistrarFormFields.CLIENT_CERTIFICATE_FIELD.convert("palfun"));
assertThat(
thrown,
equalTo(
new FormFieldException("Invalid X.509 PEM certificate")
.propagate("clientCertificate")));
}
@Test
void testValidCertificateHash_doesntThrowError() {
assertThat(
RegistrarFormFields.CLIENT_CERTIFICATE_HASH_FIELD.convert(
CertificateSamples.SAMPLE_CERT_HASH))
.hasValue(CertificateSamples.SAMPLE_CERT_HASH);
}
@Test
void testBadCertificateHash_throwsFfe() {
FormFieldException thrown =
assertThrows(
FormFieldException.class,
() -> RegistrarFormFields.CLIENT_CERTIFICATE_HASH_FIELD.convert("~~~"));
assertThat(
thrown,
equalTo(
new FormFieldException("Field must contain a base64 value.")
.propagate("clientCertificateHash")));
}
}

View File

@@ -1,371 +0,0 @@
// 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.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.whois.WhoisTestData.loadFile;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.ContactPhoneNumber;
import google.registry.model.contact.PostalInfo;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.secdns.DomainDsData;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.Host;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import google.registry.whois.WhoisResponse.WhoisResponseResults;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link DomainWhoisResponse}. */
class DomainWhoisResponseTest {
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private Host host1;
private Host host2;
private RegistrarPoc abuseContact;
private Contact adminContact;
private Contact registrant;
private Contact techContact;
private Domain domain;
private final FakeClock clock = new FakeClock(DateTime.parse("2009-05-29T20:15:00Z"));
@BeforeEach
void beforeEach() {
// Update the registrar to have an IANA ID.
Registrar registrar =
persistResource(
loadRegistrar("NewRegistrar")
.asBuilder()
.setUrl("http://my.fake.url")
.setIanaIdentifier(5555555L)
.build());
abuseContact =
persistResource(
new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName("Jake Doe")
.setEmailAddress("jakedoe@theregistrar.com")
.setPhoneNumber("+1.2125551216")
.setVisibleInDomainWhoisAsAbuse(true)
.build());
createTld("tld");
host1 =
persistResource(
new Host.Builder()
.setHostName("ns01.exampleregistrar.tld")
.setRepoId("1-ROID")
.build());
host2 =
persistResource(
new Host.Builder()
.setHostName("ns02.exampleregistrar.tld")
.setRepoId("2-ROID")
.build());
registrant =
persistResource(
new Contact.Builder()
.setContactId("5372808-ERL")
.setRepoId("4-ROID")
.setCreationRegistrarId("NewRegistrar")
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
.setLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(PostalInfo.Type.LOCALIZED)
.setName("SHOULD NOT BE USED")
.setOrg("SHOULD NOT BE USED")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 EXAMPLE STREET"))
.setCity("ANYTOWN")
.setState("AP")
.setZip("A1A1A1")
.setCountryCode("EX")
.build())
.build())
.setInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(PostalInfo.Type.INTERNATIONALIZED)
.setName("EXAMPLE REGISTRANT")
.setOrg("Tom & Jerry Corp.")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 EXAMPLE STREET"))
.setCity("ANYTOWN")
.setState("AP")
.setZip("A1A1A1")
.setCountryCode("EX")
.build())
.build())
.setVoiceNumber(
new ContactPhoneNumber.Builder()
.setPhoneNumber("+1.5555551212")
.setExtension("1234")
.build())
.setFaxNumber(
new ContactPhoneNumber.Builder()
.setPhoneNumber("+1.5555551213")
.setExtension("4321")
.build())
.setEmailAddress("EMAIL@EXAMPLE.tld")
.build());
adminContact =
persistResource(
new Contact.Builder()
.setContactId("5372809-ERL")
.setRepoId("5-ROID")
.setCreationRegistrarId("NewRegistrar")
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
.setLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(PostalInfo.Type.LOCALIZED)
.setName("SHOULD NOT BE USED")
.setOrg("SHOULD NOT BE USED")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 EXAMPLE STREET"))
.setCity("ANYTOWN")
.setState("AP")
.setZip("A1A1A1")
.setCountryCode("EX")
.build())
.build())
.setInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(PostalInfo.Type.INTERNATIONALIZED)
.setName("EXAMPLE REGISTRANT ADMINISTRATIVE")
.setOrg("EXAMPLE REGISTRANT ORGANIZATION")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 EXAMPLE STREET"))
.setCity("ANYTOWN")
.setState("AP")
.setZip("A1A1A1")
.setCountryCode("EX")
.build())
.build())
.setVoiceNumber(
new ContactPhoneNumber.Builder()
.setPhoneNumber("+1.5555551212")
.setExtension("1234")
.build())
.setFaxNumber(
new ContactPhoneNumber.Builder().setPhoneNumber("+1.5555551213").build())
.setEmailAddress("EMAIL@EXAMPLE.tld")
.build());
techContact =
persistResource(
new Contact.Builder()
.setContactId("5372811-ERL")
.setRepoId("6-ROID")
.setCreationRegistrarId("NewRegistrar")
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
.setLocalizedPostalInfo(
new PostalInfo.Builder()
.setType(PostalInfo.Type.LOCALIZED)
.setName("SHOULD NOT BE USED")
.setOrg("SHOULD NOT BE USED")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 EXAMPLE STREET"))
.setCity("ANYTOWN")
.setState("AP")
.setZip("A1A1A1")
.setCountryCode("EX")
.build())
.build())
.setInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(PostalInfo.Type.INTERNATIONALIZED)
.setName("EXAMPLE REGISTRAR TECHNICAL")
.setOrg("EXAMPLE REGISTRAR LLC")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 EXAMPLE STREET"))
.setCity("ANYTOWN")
.setState("AP")
.setZip("A1A1A1")
.setCountryCode("EX")
.build())
.build())
.setVoiceNumber(
new ContactPhoneNumber.Builder()
.setPhoneNumber("+1.1235551234")
.setExtension("1234")
.build())
.setFaxNumber(
new ContactPhoneNumber.Builder()
.setPhoneNumber("+1.5555551213")
.setExtension("93")
.build())
.setEmailAddress("EMAIL@EXAMPLE.tld")
.build());
VKey<Host> host1VKey = host1.createVKey();
VKey<Host> host2VKey = host2.createVKey();
VKey<Contact> registrantResourceKey = registrant.createVKey();
VKey<Contact> adminResourceKey = adminContact.createVKey();
VKey<Contact> techResourceKey = techContact.createVKey();
String repoId = "3-TLD";
domain =
persistResource(
new Domain.Builder()
.setDomainName("example.tld")
.setRepoId(repoId)
.setCreationRegistrarId("NewRegistrar")
.setPersistedCurrentSponsorRegistrarId("NewRegistrar")
.setLastEppUpdateTime(DateTime.parse("2009-05-29T20:13:00Z"))
.setCreationTimeForTest(DateTime.parse("2000-10-08T00:45:00Z"))
.setRegistrationExpirationTime(DateTime.parse("2010-10-08T00:44:59Z"))
.setStatusValues(
ImmutableSet.of(
StatusValue.CLIENT_DELETE_PROHIBITED,
StatusValue.CLIENT_RENEW_PROHIBITED,
StatusValue.CLIENT_TRANSFER_PROHIBITED,
StatusValue.SERVER_UPDATE_PROHIBITED))
.setRegistrant(Optional.of(registrantResourceKey))
.setContacts(
ImmutableSet.of(
DesignatedContact.create(DesignatedContact.Type.ADMIN, adminResourceKey),
DesignatedContact.create(DesignatedContact.Type.TECH, techResourceKey)))
.setNameservers(ImmutableSet.of(host1VKey, host2VKey))
.setDsData(ImmutableSet.of(DomainDsData.create(1, 2, 3, "deadface")))
.setGracePeriods(
ImmutableSet.of(
GracePeriod.create(
GracePeriodStatus.ADD, repoId, END_OF_TIME, "NewRegistrar", null),
GracePeriod.create(
GracePeriodStatus.TRANSFER, repoId, END_OF_TIME, "NewRegistrar", null)))
.build());
}
@Test
void getPlainTextOutputTest() {
DomainWhoisResponse domainWhoisResponse =
new DomainWhoisResponse(domain, false, "Please contact registrar", clock.nowUtc());
assertThat(
domainWhoisResponse.getResponse(
false,
"Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested."))
.isEqualTo(WhoisResponseResults.create(loadFile("whois_domain.txt"), 1));
}
@Test
void getPlainTextOutputTest_noRegistrant() {
DomainWhoisResponse domainWhoisResponse =
new DomainWhoisResponse(
domain.asBuilder().setRegistrant(Optional.empty()).build(),
false,
"Please contact registrar",
clock.nowUtc());
assertThat(
domainWhoisResponse.getResponse(
false,
"Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested."))
.isEqualTo(WhoisResponseResults.create(loadFile("whois_domain_no_registrant.txt"), 1));
}
@Test
void getPlainTextOutputTest_noContacts() {
DomainWhoisResponse domainWhoisResponse =
new DomainWhoisResponse(
domain
.asBuilder()
.setRegistrant(Optional.empty())
.setContacts(ImmutableSet.of())
.build(),
false,
"Please contact registrar",
clock.nowUtc());
assertThat(
domainWhoisResponse.getResponse(
false,
"Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested."))
.isEqualTo(WhoisResponseResults.create(loadFile("whois_domain_no_contacts.txt"), 1));
}
@Test
void getPlainTextOutputTest_registrarAbuseInfoMissing() {
persistResource(abuseContact.asBuilder().setVisibleInDomainWhoisAsAbuse(false).build());
DomainWhoisResponse domainWhoisResponse =
new DomainWhoisResponse(domain, false, "Please contact registrar", clock.nowUtc());
assertThat(
domainWhoisResponse.getResponse(false, "Footer"))
.isEqualTo(
WhoisResponseResults.create(
loadFile("whois_domain_registrar_abuse_info_missing.txt"), 1));
}
@Test
void getPlainTextOutputTest_fullOutput() {
DomainWhoisResponse domainWhoisResponse =
new DomainWhoisResponse(domain, true, "Please contact registrar", clock.nowUtc());
assertThat(
domainWhoisResponse.getResponse(
false,
"Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested."))
.isEqualTo(WhoisResponseResults.create(loadFile("whois_domain_full_output.txt"), 1));
}
@Test
void addImplicitOkStatusTest() {
DomainWhoisResponse domainWhoisResponse =
new DomainWhoisResponse(
domain.asBuilder().setStatusValues(null).build(),
false,
"Contact the registrar",
clock.nowUtc());
assertThat(
domainWhoisResponse
.getResponse(
false,
"""
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.""")
.plainTextOutput())
.contains("Domain Status: ok");
}
}

View File

@@ -1,127 +0,0 @@
// 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.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistNewRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.whois.WhoisTestData.loadFile;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InetAddresses;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.registrar.Registrar;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import google.registry.whois.WhoisResponse.WhoisResponseResults;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link NameserverWhoisResponse}. */
class NameserverWhoisResponseTest {
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private Host host1;
private Host host2;
private Host host3;
private final FakeClock clock = new FakeClock(DateTime.parse("2009-05-29T20:15:00Z"));
@BeforeEach
void beforeEach() {
persistNewRegistrar("example", "Hänsel & Gretel Registrar, Inc.", Registrar.Type.REAL, 8L);
persistResource(loadRegistrar("example").asBuilder().setUrl("http://my.fake.url").build());
createTld("tld");
Domain domain = persistResource(DatabaseHelper.newDomain("zobo.tld"));
host1 =
new Host.Builder()
.setHostName("ns1.example.tld")
.setPersistedCurrentSponsorRegistrarId("example")
.setInetAddresses(
ImmutableSet.of(
InetAddresses.forString("192.0.2.123"),
InetAddresses.forString("2001:0DB8::1")))
.setRepoId("1-EXAMPLE")
.build();
host2 =
new Host.Builder()
.setHostName("ns2.example.tld")
.setPersistedCurrentSponsorRegistrarId("example")
.setInetAddresses(
ImmutableSet.of(
InetAddresses.forString("192.0.2.123"),
InetAddresses.forString("2001:0DB8::1")))
.setRepoId("2-EXAMPLE")
.build();
host3 =
new Host.Builder()
.setHostName("ns1.zobo.tld")
.setSuperordinateDomain(domain.createVKey())
.setPersistedCurrentSponsorRegistrarId("example")
.setInetAddresses(
ImmutableSet.of(
InetAddresses.forString("192.0.2.123"),
InetAddresses.forString("2001:0DB8::1")))
.setRepoId("3-EXAMPLE")
.build();
}
@Test
void testGetTextOutput() {
NameserverWhoisResponse nameserverWhoisResponse =
new NameserverWhoisResponse(host1, clock.nowUtc());
assertThat(
nameserverWhoisResponse.getResponse(
false,
"Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested."))
.isEqualTo(WhoisResponseResults.create(loadFile("whois_nameserver.txt"), 1));
}
@Test
void testGetMultipleNameserversResponse() {
NameserverWhoisResponse nameserverWhoisResponse =
new NameserverWhoisResponse(ImmutableList.of(host1, host2), clock.nowUtc());
assertThat(
nameserverWhoisResponse.getResponse(
false,
"Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested."))
.isEqualTo(WhoisResponseResults.create(loadFile("whois_multiple_nameservers.txt"), 2));
}
@Test
void testSubordinateDomains() {
NameserverWhoisResponse nameserverWhoisResponse =
new NameserverWhoisResponse(host3, clock.nowUtc());
assertThat(
nameserverWhoisResponse.getResponse(
false,
"Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested."))
.isEqualTo(WhoisResponseResults.create(loadFile("whois_subord_nameserver.txt"), 1));
}
}

View File

@@ -1,138 +0,0 @@
// 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.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.persistNewRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DatabaseHelper.persistResources;
import static google.registry.whois.WhoisTestData.loadFile;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import google.registry.whois.WhoisResponse.WhoisResponseResults;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link RegistrarWhoisResponse}. */
class RegistrarWhoisResponseTest {
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private final FakeClock clock = new FakeClock(DateTime.parse("2009-05-29T20:15:00Z"));
@Test
void getTextOutputTest() {
Registrar registrar =
new Registrar.Builder()
.setRegistrarId("exregistrar")
.setRegistrarName("Example Registrar, Inc.")
.setType(Registrar.Type.REAL)
.setIanaIdentifier(8L)
.setState(Registrar.State.ACTIVE)
.setLocalizedAddress(
new RegistrarAddress.Builder()
.setStreet(ImmutableList.of("1234 Admiralty Way"))
.setCity("Marina del Rey")
.setState("CA")
.setZip("90292")
.setCountryCode("US")
.build())
.setPhoneNumber("+1.3105551212")
.setFaxNumber("+1.3105551213")
.setEmailAddress("registrar@example.tld")
.setWhoisServer("whois.example-registrar.tld")
.setUrl("http://my.fake.url")
.build();
// Use the registrar key for contacts' parent.
ImmutableList<RegistrarPoc> contacts =
ImmutableList.of(
new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName("Joe Registrar")
.setEmailAddress("joeregistrar@example-registrar.tld")
.setPhoneNumber("+1.3105551213")
.setFaxNumber("+1.3105551213")
.setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN))
.setVisibleInWhoisAsAdmin(true)
.setVisibleInWhoisAsTech(false)
.build(),
new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName("John Doe")
.setEmailAddress("johndoe@example-registrar.tld")
.setPhoneNumber("+1.1111111111")
.setFaxNumber("+1.1111111111")
.setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN))
.build(),
new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName("Jane Registrar")
.setEmailAddress("janeregistrar@example-registrar.tld")
.setPhoneNumber("+1.3105551214")
.setFaxNumber("+1.3105551213")
.setTypes(ImmutableSet.of(RegistrarPoc.Type.ADMIN))
.setVisibleInWhoisAsAdmin(true)
.build(),
new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName("Jane Doe")
.setEmailAddress("janedoe@example-registrar.tld")
.setPhoneNumber("+1.1111111112")
.setFaxNumber("+1.1111111112")
.setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH))
.build(),
new RegistrarPoc.Builder()
.setRegistrar(registrar)
.setName("Bonnie & Clyde")
.setEmailAddress("johngeek@example-registrar.tld")
.setPhoneNumber("+1.3105551215")
.setFaxNumber("+1.3105551216")
.setTypes(ImmutableSet.of(RegistrarPoc.Type.TECH))
.setVisibleInWhoisAsTech(true)
.build());
persistResource(registrar);
persistResources(contacts);
RegistrarWhoisResponse registrarWhoisResponse =
new RegistrarWhoisResponse(registrar, clock.nowUtc());
assertThat(
registrarWhoisResponse.getResponse(
false,
"Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested."))
.isEqualTo(WhoisResponseResults.create(loadFile("whois_registrar.txt"), 1));
}
@Test
void testSetOfFields() {
Registrar registrar =
persistNewRegistrar("exregistrar", "Ex-Registrar", Registrar.Type.REAL, 8L);
RegistrarWhoisResponse registrarWhoisResponse =
new RegistrarWhoisResponse(registrar, clock.nowUtc());
// Just make sure this doesn't NPE.
registrarWhoisResponse.getResponse(
false, "Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested.");
}
}

View File

@@ -1,679 +0,0 @@
// 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.truth.Truth.assertThat;
import static google.registry.bsa.persistence.BsaTestingUtils.persistBsaLabel;
import static google.registry.model.EppResourceUtils.loadByForeignKeyByCacheIfEnabled;
import static google.registry.model.registrar.Registrar.State.ACTIVE;
import static google.registry.model.registrar.Registrar.Type.PDT;
import static google.registry.model.tld.Tlds.getTlds;
import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DatabaseHelper.persistResources;
import static google.registry.testing.FullFieldsTestEntityHelper.makeDomain;
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrar;
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrarPocs;
import static google.registry.whois.WhoisTestData.loadFile;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InetAddresses;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.Host;
import google.registry.model.registrar.Registrar;
import google.registry.model.tld.Tld;
import google.registry.model.transfer.DomainTransferData;
import google.registry.model.transfer.TransferStatus;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.testing.FakeSleeper;
import google.registry.testing.FullFieldsTestEntityHelper;
import google.registry.testing.TestCacheExtension;
import google.registry.util.Retrier;
import google.registry.whois.WhoisMetrics.WhoisMetric;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.time.Duration;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link WhoisAction}. */
public class WhoisActionTest {
private final FakeClock clock = new FakeClock(DateTime.parse("2009-06-29T20:13:00Z"));
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
@RegisterExtension
public final TestCacheExtension testCacheExtension =
new TestCacheExtension.Builder()
.withEppResourceCache(Duration.ofDays(1))
.withForeignKeyCache(Duration.ofDays(1))
.build();
private final FakeResponse response = new FakeResponse();
private WhoisAction newWhoisAction(String input) {
WhoisAction whoisAction = new WhoisAction();
whoisAction.clock = clock;
whoisAction.input = new StringReader(input);
whoisAction.response = response;
whoisAction.whoisReader =
new WhoisReader(
WhoisCommandFactory.createCached(), "Please contact registrar", "Blocked by BSA: %s");
whoisAction.whoisMetrics = new WhoisMetrics();
whoisAction.metricBuilder = WhoisMetric.builderForRequest(clock);
whoisAction.disclaimer =
"Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested.";
whoisAction.retrier = new Retrier(new FakeSleeper(clock), 3);
return whoisAction;
}
@BeforeEach
void setUp() {
createTlds("lol", "xn--q9jyb4c", "1.test");
}
@Test
void testRun_badRequest_stillSends200() {
newWhoisAction("\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_no_command.txt"));
}
private static Domain makeDomainWithRegistrar(Registrar registrar) {
return makeDomain(
"cat.lol",
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-ERL", "Goblin Market", "lol@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-IRL", "Santa Claus", "BOFH@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-TRL", "The Raven", "bog@cat.lol")),
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.cat.lol", "bad:f00d:cafe::15:beef")),
registrar);
}
@Test
void testRun_domainQuery_works() {
Registrar registrar =
persistResource(makeRegistrar("evilregistrar", "Yes Virginia", ACTIVE));
persistResource(makeDomainWithRegistrar(registrar));
persistResources(makeRegistrarPocs(registrar));
newWhoisAction("domain cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_domain.txt"));
}
@Test
void testRun_domainQuery_usesCache() {
Registrar registrar =
persistResource(makeRegistrar("evilregistrar", "Yes Virginia", ACTIVE));
persistResource(makeDomainWithRegistrar(registrar));
persistResources(makeRegistrarPocs(registrar));
// Populate the cache for both the domain and contact.
Domain domain = loadByForeignKeyByCacheIfEnabled(Domain.class, "cat.lol", clock.nowUtc()).get();
Contact contact =
loadByForeignKeyByCacheIfEnabled(Contact.class, "5372808-ERL", clock.nowUtc()).get();
// Make a change to the domain and contact that won't be seen because the cache will be hit.
persistResource(domain.asBuilder().setDeletionTime(clock.nowUtc().minusDays(1)).build());
persistResource(
contact
.asBuilder()
.setInternationalizedPostalInfo(
contact
.getInternationalizedPostalInfo()
.asBuilder()
.setOrg("Two by Two, Hands Blue Inc.")
.build())
.build());
newWhoisAction("domain cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_domain.txt"));
}
@Test
void testRun_domainQuery_registeredDomainUnaffectedByBsa() {
persistResource(
Tld.get("lol")
.asBuilder()
.setBsaEnrollStartTime(Optional.of(clock.nowUtc().minusDays(1)))
.build());
persistBsaLabel("cat");
Registrar registrar = persistResource(makeRegistrar("evilregistrar", "Yes Virginia", ACTIVE));
persistResource(makeDomainWithRegistrar(registrar));
persistResources(makeRegistrarPocs(registrar));
newWhoisAction("domain cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_domain.txt"));
}
@Test
void testRun_domainQuery_unregisteredDomainShowBsaMessage() {
persistResource(
Tld.get("lol")
.asBuilder()
.setBsaEnrollStartTime(Optional.of(clock.nowUtc().minusDays(1)))
.build());
persistBsaLabel("cat");
newWhoisAction("domain cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_domain_blocked_by_bsa.txt"));
}
@Test
void testRun_domainAfterTransfer_hasUpdatedEppTimeAndClientId() {
Registrar registrar = persistResource(makeRegistrar("TheRegistrar", "Yes Virginia", ACTIVE));
persistResource(
makeDomainWithRegistrar(registrar)
.asBuilder()
.setTransferData(
new DomainTransferData.Builder()
.setGainingRegistrarId("TheRegistrar")
.setLosingRegistrarId("NewRegistrar")
.setTransferRequestTime(DateTime.parse("2009-05-29T20:13:00Z"))
.setPendingTransferExpirationTime(DateTime.parse("2010-03-01T00:00:00Z"))
.setTransferStatus(TransferStatus.PENDING)
.setTransferRequestTrid(Trid.create("client-trid", "server-trid"))
.build())
.build());
persistResources(makeRegistrarPocs(registrar));
clock.setTo(DateTime.parse("2011-01-01T00:00:00Z"));
newWhoisAction("domain cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_transferred_domain.txt"));
}
@Test
void testRun_idnDomain_works() {
Registrar registrar = persistResource(makeRegistrar(
"evilregistrar", "Yes Virginia", ACTIVE));
persistResource(
makeDomain(
"cat.みんな",
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-ERL", "(◕‿◕)", "lol@cat.みんな")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-IRL", "Santa Claus", "BOFH@cat.みんな")),
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-TRL", "The Raven", "bog@cat.みんな")),
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.みんな", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.cat.みんな", "bad:f00d:cafe::15:beef")),
registrar));
persistResources(makeRegistrarPocs(registrar));
newWhoisAction("domain cat.みんな\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_idn_punycode.txt"));
}
@Test
void testRun_punycodeDomain_works() {
Registrar registrar = persistResource(makeRegistrar(
"evilregistrar", "Yes Virginia", ACTIVE));
persistResource(
makeDomain(
"cat.みんな",
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-ERL", "(◕‿◕)", "lol@cat.みんな")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-IRL", "Santa Claus", "BOFH@cat.みんな")),
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-TRL", "The Raven", "bog@cat.みんな")),
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.みんな", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.cat.みんな", "bad:f00d:cafe::15:beef")),
registrar));
persistResources(makeRegistrarPocs(registrar));
newWhoisAction("domain cat.xn--q9jyb4c\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_idn_punycode.txt"));
}
@Test
void testRun_domainNotFound_returns200OkAndPlainTextResponse() {
newWhoisAction("domain cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_domain_not_found.txt"));
}
@Test
void testRun_domainNotFound_usesCache() {
// Populate the cache with the nonexistence of this domain.
assertThat(loadByForeignKeyByCacheIfEnabled(Domain.class, "cat.lol", clock.nowUtc())).isEmpty();
// Add a new valid cat.lol domain that won't be found because the cache will be hit instead.
persistActiveDomain("cat.lol");
newWhoisAction("domain cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_domain_not_found.txt"));
}
// todo (b/27378695): reenable or delete this test
@Disabled
@Test
void testRun_domainInTestTld_isConsideredNotFound() {
persistResource(Tld.get("lol").asBuilder().setTldType(Tld.TldType.TEST).build());
Registrar registrar = persistResource(makeRegistrar(
"evilregistrar", "Yes Virginia", ACTIVE));
persistResource(
makeDomain(
"cat.lol",
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-ERL", "Goblin Market", "lol@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-IRL", "Santa Claus", "BOFH@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-TRL", "The Raven", "bog@cat.lol")),
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.cat.lol", "bad:f00d:cafe::15:beef")),
registrar));
persistResources(makeRegistrarPocs(registrar));
newWhoisAction("domain cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_domain_not_found.txt"));
}
@Test
void testRun_domainFlaggedAsDeletedInDatabase_isConsideredNotFound() {
Registrar registrar;
persistResource(
makeDomain(
"cat.lol",
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-ERL", "Peter Murphy", "lol@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-IRL", "Santa Claus", "BOFH@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-TRL", "The Raven", "bog@cat.lol")),
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.cat.lol", "bad:f00d:cafe::15:beef")),
persistResource(registrar = makeRegistrar("example", "Example Registrar", ACTIVE)))
.asBuilder()
.setDeletionTime(clock.nowUtc().minusDays(1))
.build());
persistResources(makeRegistrarPocs(registrar));
newWhoisAction("domain cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_domain_not_found.txt"));
}
/**
* Create a deleted domain and an active domain with the same label, and make sure only the active
* one is returned.
*/
@Test
void testRun_domainDeletedThenRecreated_isFound() {
Registrar registrar;
Domain domain1 =
persistResource(
makeDomain(
"cat.lol",
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-ERL", "Peter Murphy", "lol@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-IRL", "Santa Claus", "BOFH@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-TRL", "The Raven", "bog@cat.lol")),
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost(
"ns2.cat.lol", "bad:f00d:cafe::15:beef")),
persistResource(makeRegistrar("example", "Example Registrar", ACTIVE)))
.asBuilder()
.setCreationTimeForTest(clock.nowUtc().minusDays(2))
.setDeletionTime(clock.nowUtc().minusDays(1))
.build());
Domain domain2 =
persistResource(
makeDomain(
"cat.lol",
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372809-ERL", "Mrs. Alice Crypto", "alice@example.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372809-IRL", "Mr. Bob Crypto", "bob@example.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372809-TRL", "Dr. Pablo", "pmy@example.lol")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns1.google.lol", "9.9.9.9")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.google.lol", "4311::f143")),
persistResource(
registrar = makeRegistrar("example", "Example Registrar", ACTIVE)))
.asBuilder()
.setCreationTimeForTest(clock.nowUtc())
.build());
persistResources(makeRegistrarPocs(registrar));
assertThat(domain1.getRepoId()).isNotEqualTo(domain2.getRepoId());
newWhoisAction("domain cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).contains("ns1.google.lol");
}
@Test
void testRun_nameserverQuery_works() {
persistResource(loadRegistrar("TheRegistrar").asBuilder().setUrl("http://my.fake.url").build());
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
newWhoisAction("nameserver ns1.cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_nameserver.txt"));
}
@Test
void testRun_ipv6_displaysInCollapsedReadableFormat() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "bad:f00d:cafe::15:beef"));
newWhoisAction("nameserver ns1.cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).contains("ns1.cat.lol");
// The most important thing this tests is that the outputted address is compressed!
assertThat(response.getPayload()).contains("bad:f00d:cafe::15:beef");
assertThat(response.getPayload()).doesNotContain("bad:f00d:cafe:0:0:0:15:beef");
}
@Test
void testRun_idnNameserver_works() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.みんな", "1.2.3.4"));
newWhoisAction("nameserver ns1.cat.みんな\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).contains("ns1.cat.xn--q9jyb4c");
assertThat(response.getPayload()).contains("1.2.3.4");
}
@Test
void testRun_nameserver_usesCache() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.xn--q9jyb4c", "1.2.3.4"));
// Populate the cache.
Host host =
loadByForeignKeyByCacheIfEnabled(Host.class, "ns1.cat.xn--q9jyb4c", clock.nowUtc()).get();
// Make a change to the persisted host that won't be seen because the cache will be hit.
persistResource(
host.asBuilder()
.setInetAddresses(ImmutableSet.of(InetAddresses.forString("8.8.8.8")))
.build());
newWhoisAction("nameserver ns1.cat.みんな\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).contains("ns1.cat.xn--q9jyb4c");
assertThat(response.getPayload()).contains("1.2.3.4");
}
@Test
void testRun_punycodeNameserver_works() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.みんな", "1.2.3.4"));
newWhoisAction("nameserver ns1.cat.xn--q9jyb4c\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).contains("ns1.cat.xn--q9jyb4c");
assertThat(response.getPayload()).contains("1.2.3.4");
}
@Test
void testRun_nameserverNotFound_returns200AndText() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
newWhoisAction("nameserver ns1.cat.lulz\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_nameserver_not_found.txt"));
}
@Test
void testRun_nameserverFlaggedAsDeletedInDatabase_doesntGetLeaked() {
persistResource(
FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4")
.asBuilder()
.setDeletionTime(clock.nowUtc().minusDays(1))
.build());
newWhoisAction("nameserver ns1.cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_nameserver_not_found.txt"));
}
@Test
void testRun_ipNameserverLookup_works() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
newWhoisAction("nameserver 1.2.3.4").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).contains("ns1.cat.lol");
}
@Test
void testRun_ipMapsToMultipleNameservers_theyAllGetReturned() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
persistResource(FullFieldsTestEntityHelper.makeHost("ns2.cat.lol", "1.2.3.4"));
newWhoisAction("nameserver 1.2.3.4").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).contains("ns1.cat.lol");
assertThat(response.getPayload()).contains("ns2.cat.lol");
}
@Test
void testRun_ipMapsToMultipleNameserverInDifferentTlds_showsThemAll() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.xn--q9jyb4c", "1.2.3.4"));
newWhoisAction("nameserver 1.2.3.4").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).contains("ns1.cat.lol");
assertThat(response.getPayload()).contains("ns1.cat.xn--q9jyb4c");
}
@Test
void testRun_ipNameserverEntityDoesNotExist_returns200NotFound() {
newWhoisAction("nameserver feed:a:bee::acab\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_ip_not_found.txt"));
}
@Test
void testRun_ipMapsToNameserverUnderNonAuthoritativeTld_notFound() {
assertThat(getTlds()).doesNotContain("com");
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.google.com", "1.2.3.4"));
newWhoisAction("nameserver 1.2.3.4").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_ip_not_found.txt"));
}
@Test
void testRun_nameserverUnderNonAuthoritativeTld_notFound() {
assertThat(getTlds()).doesNotContain("com");
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.google.com", "1.2.3.4"));
newWhoisAction("nameserver ns1.google.com").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_nameserver_not_found.txt"));
}
// todo (b/27378695): reenable or delete this test
@Disabled
@Test
void testRun_nameserverInTestTld_notFound() {
persistResource(Tld.get("lol").asBuilder().setTldType(Tld.TldType.TEST).build());
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
newWhoisAction("nameserver ns1.cat.lol").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_nameserver_not_found.txt"));
}
@Test
void testRun_registrarLookup_works() {
Registrar registrar = persistResource(
makeRegistrar("example", "Example Registrar, Inc.", ACTIVE));
persistResources(makeRegistrarPocs(registrar));
// Notice the partial search without "inc".
newWhoisAction("registrar example registrar").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_registrar.txt"));
}
@Test
void testRun_pdtRegistrarLookup_works() {
Registrar registrar =
persistResource(
makeRegistrar("example", "Example Registrar, Inc.", ACTIVE)
.asBuilder()
.setIanaIdentifier(9995L)
.setType(PDT)
.build());
persistResources(makeRegistrarPocs(registrar));
// Notice the partial search without "inc".
newWhoisAction("registrar example registrar").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_registrar.txt"));
}
@Test
void testRun_registrarLookupInPendingState_returnsNotFound() {
Registrar registrar = persistResource(
makeRegistrar("example", "Example Registrar, Inc.", Registrar.State.PENDING));
persistResources(makeRegistrarPocs(registrar));
newWhoisAction("registrar Example Registrar, Inc.").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_registrar_not_found.txt"));
}
@Test
void testRun_registrarLookupWithTestType_returnsNotFound() {
Registrar registrar = persistResource(
makeRegistrar("example", "Example Registrar, Inc.", ACTIVE)
.asBuilder()
.setIanaIdentifier(null)
.setType(Registrar.Type.TEST)
.build());
persistResources(makeRegistrarPocs(registrar));
newWhoisAction("registrar Example Registrar, Inc.").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_registrar_not_found.txt"));
}
@Test
void testRun_multilevelDomain_isNotConsideredAHostname() {
Registrar registrar =
persistResource(makeRegistrar("example", "Example Registrar", ACTIVE));
persistResource(
makeDomain(
"cat.1.test",
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-ERL", "(◕‿◕)", "lol@cat.1.test")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-IRL", "Santa Claus", "BOFH@cat.1.test")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-TRL", "The Raven", "bog@cat.1.test")),
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.1.test", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.cat.1.test", "bad:f00d:cafe::15:beef")),
registrar));
persistResources(makeRegistrarPocs(registrar));
newWhoisAction("domain cat.1.test\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).contains("Domain Name: cat.1.test\r\n");
}
@Test
void testRun_hostnameWithMultilevelTld_isStillConsideredHostname() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.1.test", "1.2.3.4"));
newWhoisAction("nameserver ns1.cat.1.test\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).contains("ns1.cat.1.test");
assertThat(response.getPayload()).contains("1.2.3.4");
}
@Test
void testRun_metricsLoggedForSuccessfulCommand() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
persistResource(FullFieldsTestEntityHelper.makeHost("ns2.cat.lol", "1.2.3.4"));
WhoisAction action = newWhoisAction("nameserver 1.2.3.4");
action.whoisMetrics = mock(WhoisMetrics.class);
action.run();
WhoisMetric expected =
WhoisMetric.builderForRequest(clock)
.setCommandName("NameserverLookupByIp")
.setNumResults(2)
.setStatus(SC_OK)
.build();
verify(action.whoisMetrics).recordWhoisMetric(eq(expected));
}
@Test
void testRun_metricsLoggedForUnsuccessfulCommand() {
WhoisAction action = newWhoisAction("domain cat.lol\r\n");
action.whoisMetrics = mock(WhoisMetrics.class);
action.run();
WhoisMetric expected =
WhoisMetric.builderForRequest(clock)
.setCommandName("DomainLookup")
.setNumResults(0)
.setStatus(SC_NOT_FOUND)
.build();
verify(action.whoisMetrics).recordWhoisMetric(eq(expected));
}
@Test
void testRun_metricsLoggedForInternalServerError() throws Exception {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
WhoisAction action = newWhoisAction("ns1.cat.lol");
action.whoisReader = mock(WhoisReader.class);
when(action.whoisReader.readCommand(any(Reader.class), eq(false), any(DateTime.class)))
.thenThrow(new IOException("missing cat interface"));
action.whoisMetrics = mock(WhoisMetrics.class);
action.run();
WhoisMetric expected =
WhoisMetric.builderForRequest(clock)
.setNumResults(0)
.setStatus(SC_INTERNAL_SERVER_ERROR)
.build();
verify(action.whoisMetrics).recordWhoisMetric(eq(expected));
assertThat(response.getPayload()).isEqualTo("Internal Server Error");
}
}

View File

@@ -1,244 +0,0 @@
// Copyright 2021 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.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.newHost;
import static google.registry.testing.DatabaseHelper.newTld;
import static google.registry.testing.DatabaseHelper.persistNewRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InternetDomainName;
import google.registry.config.RegistryConfig;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.registrar.Registrar;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import google.registry.testing.TestCacheExtension;
import java.net.InetAddress;
import java.time.Duration;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
class WhoisCommandFactoryTest {
FakeClock clock = new FakeClock();
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
@RegisterExtension
final TestCacheExtension testCacheExtension =
new TestCacheExtension.Builder().withEppResourceCache(Duration.ofSeconds(1000000)).build();
WhoisCommandFactory noncachedFactory = WhoisCommandFactory.createNonCached();
WhoisCommandFactory cachedFactory = WhoisCommandFactory.createCached();
Domain domain;
Host host;
Registrar otherRegistrar;
int origSingletonCacheRefreshSeconds;
@BeforeEach
void setUp() throws Exception {
persistResource(newTld("tld", "TLD"));
host =
newHost("ns.example.tld")
.asBuilder()
.setInetAddresses(ImmutableSet.of(InetAddress.getByName("1.2.3.4")))
.build();
persistResource(host);
domain = DatabaseHelper.newDomain("example.tld", host);
persistResource(domain);
otherRegistrar = persistNewRegistrar("OtherRegistrar");
otherRegistrar =
persistResource(
otherRegistrar
.asBuilder()
.setState(Registrar.State.ACTIVE)
.setPhoneNumber("+1.2223334444")
.build());
// In addition to the TestCacheExtension, we have to set a long singleton cache timeout.
RegistryConfig.CONFIG_SETTINGS.get().caching.singletonCacheRefreshSeconds = 1000000;
}
@AfterEach
void tearDown() {
// Restore the singleton cache timeout. For some reason, this doesn't work if we store the
// original value in an instance variable (I suspect there may be some overlap in test
// execution) so just restore to zero.
RegistryConfig.CONFIG_SETTINGS.get().caching.singletonCacheRefreshSeconds = 0;
}
@Test
void testNonCached_NameserverLookupByHostCommand() throws Exception {
WhoisResponse response =
noncachedFactory
.nameserverLookupByHost(InternetDomainName.from("ns.example.tld"))
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: The Registrar");
// Note that we can't use persistResource() for these as that clears the cache.
tm().transact(
() ->
tm().put(
host.asBuilder()
.setPersistedCurrentSponsorRegistrarId("OtherRegistrar")
.build()));
response =
noncachedFactory
.nameserverLookupByHost(InternetDomainName.from("ns.example.tld"))
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: OtherRegistrar name");
}
@Test
void testCached_NameserverLookupByHostCommand() throws Exception {
WhoisResponse response =
cachedFactory
.nameserverLookupByHost(InternetDomainName.from("ns.example.tld"))
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: The Registrar");
tm().transact(
() ->
tm().put(
host.asBuilder()
.setPersistedCurrentSponsorRegistrarId("OtherRegistrar")
.build()));
response =
cachedFactory
.nameserverLookupByHost(InternetDomainName.from("ns.example.tld"))
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: The Registrar");
}
@Test
void testNonCached_DomainLookupCommand() throws Exception {
WhoisResponse response =
noncachedFactory
.domainLookup(
InternetDomainName.from("example.tld"), true, "REDACTED", "Blocked by BSA")
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: The Registrar");
tm().transact(
() ->
tm().put(
domain
.asBuilder()
.setPersistedCurrentSponsorRegistrarId("OtherRegistrar")
.build()));
response =
noncachedFactory
.domainLookup(
InternetDomainName.from("example.tld"), true, "REDACTED", "Blocked by BSA")
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: OtherRegistrar name");
}
@Test
void testCached_DomainLookupCommand() throws Exception {
WhoisResponse response =
cachedFactory
.domainLookup(InternetDomainName.from("example.tld"), true, "REDACTED", "Not tested")
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: The Registrar");
tm().transact(
() ->
tm().put(
domain
.asBuilder()
.setPersistedCurrentSponsorRegistrarId("OtherRegistrar")
.build()));
response =
cachedFactory
.domainLookup(
InternetDomainName.from("example.tld"), true, "REDACTED", "Blocked by BSA")
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: The Registrar");
}
@Test
void testNonCached_RegistrarLookupCommand() throws Exception {
WhoisResponse response =
noncachedFactory.registrarLookup("OtherRegistrar").executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Phone Number: +1.2223334444");
tm().transact(
() -> tm().put(otherRegistrar.asBuilder().setPhoneNumber("+1.2345677890").build()));
response = noncachedFactory.registrarLookup("OtherRegistrar").executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Phone Number: +1.2345677890");
}
@Test
void testCached_RegistrarLookupCommand() throws Exception {
WhoisResponse response =
cachedFactory.registrarLookup("OtherRegistrar").executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Phone Number: +1.2223334444");
tm().transact(
() -> tm().put(otherRegistrar.asBuilder().setPhoneNumber("+1.2345677890").build()));
response = cachedFactory.registrarLookup("OtherRegistrar").executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Phone Number: +1.2223334444");
}
@Test
void testNonCached_NameserverLookupByIpCommand() throws Exception {
// Note that this lookup currently doesn't cache the hosts, so there's no point in testing the
// "cached" case. This test is here so that it will fail if anyone adds caching.
WhoisResponse response =
noncachedFactory
.nameserverLookupByIp(InetAddress.getByName("1.2.3.4"))
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: The Registrar");
tm().transact(
() ->
tm().put(
host.asBuilder()
.setPersistedCurrentSponsorRegistrarId("OtherRegistrar")
.build()));
response =
noncachedFactory
.nameserverLookupByIp(InetAddress.getByName("1.2.3.4"))
.executeQuery(clock.nowUtc());
assertThat(response.getResponse(false, "").plainTextOutput())
.contains("Registrar: OtherRegistrar");
}
}

View File

@@ -1,466 +0,0 @@
// 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.net.MediaType.PLAIN_TEXT_UTF_8;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.bsa.persistence.BsaTestingUtils.persistBsaLabel;
import static google.registry.model.registrar.Registrar.State.ACTIVE;
import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DatabaseHelper.persistResources;
import static google.registry.testing.FullFieldsTestEntityHelper.makeDomain;
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrar;
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrarPocs;
import static google.registry.whois.WhoisTestData.loadFile;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import google.registry.model.contact.Contact;
import google.registry.model.registrar.Registrar;
import google.registry.model.tld.Tld;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.testing.FullFieldsTestEntityHelper;
import google.registry.whois.WhoisMetrics.WhoisMetric;
import java.io.IOException;
import java.io.Reader;
import java.util.Optional;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/**
* Unit tests for {@link WhoisHttpAction}.
*
* <p>This class should be limited to testing the HTTP interface, as the bulk of the WHOIS testing
* can be found in {@link WhoisActionTest}.
*/
class WhoisHttpActionTest {
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private final FakeResponse response = new FakeResponse();
private final FakeClock clock = new FakeClock(DateTime.parse("2009-06-29T20:13:00Z"));
private WhoisHttpAction newWhoisHttpAction(String pathInfo) {
WhoisHttpAction whoisAction = new WhoisHttpAction();
whoisAction.clock = clock;
whoisAction.expires = Duration.standardHours(1);
whoisAction.requestPath = WhoisHttpAction.PATH + pathInfo;
whoisAction.response = response;
whoisAction.whoisReader =
new WhoisReader(
WhoisCommandFactory.createCached(), "Please contact registrar", "Blocked by BSA: %s");
whoisAction.whoisMetrics = new WhoisMetrics();
whoisAction.metricBuilder = WhoisMetric.builderForRequest(clock);
whoisAction.disclaimer =
"Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested.";
return whoisAction;
}
@BeforeEach
void beforeEach() {
createTlds("lol", "xn--q9jyb4c", "1.test");
}
@Test
void testRun_emptyQuery_returns400BadRequestWithPlainTextOutput() {
newWhoisHttpAction("").run();
assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_no_command.txt"));
}
@Test
void testRun_badUrlEncoding_returns400BadRequestWithPlainTextOutput() {
newWhoisHttpAction("nic.%u307F%u3093%u306A").run();
assertThat(response.getStatus()).isEqualTo(400);
assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_malformed_path.txt"));
}
@Test
void testRun_domainNotFound_returns404StatusAndPlainTextResponse() {
newWhoisHttpAction("/domain/cat.lol").run();
assertThat(response.getStatus()).isEqualTo(404);
assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_domain_not_found.txt"));
}
// todo (b/27378695): reenable or delete this test
@Disabled
@Test
void testRun_domainInTestTld_isConsideredNotFound() {
persistResource(Tld.get("lol").asBuilder().setTldType(Tld.TldType.TEST).build());
Registrar registrar = persistResource(makeRegistrar(
"evilregistrar", "Yes Virginia", Registrar.State.ACTIVE));
persistResource(
makeDomain(
"cat.lol",
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-ERL", "Goblin Market", "lol@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-IRL", "Santa Claus", "BOFH@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-TRL", "The Raven", "bog@cat.lol")),
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.cat.lol", "bad:f00d:cafe::15:beef")),
registrar));
persistResources(makeRegistrarPocs(registrar));
newWhoisHttpAction("/domain/cat.lol").run();
assertThat(response.getStatus()).isEqualTo(404);
assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_domain_not_found.txt"));
}
@Test
void testRun_domainQueryIdn_works() {
Registrar registrar = persistResource(makeRegistrar(
"evilregistrar", "Yes Virginia", Registrar.State.ACTIVE));
persistResource(
makeDomain(
"cat.みんな",
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-ERL", "(◕‿◕)", "lol@cat.みんな")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-IRL", "Santa Claus", "BOFH@cat.みんな")),
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-TRL", "The Raven", "bog@cat.みんな")),
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.みんな", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.cat.みんな", "bad:f00d:cafe::15:beef")),
registrar));
persistResources(makeRegistrarPocs(registrar));
newWhoisHttpAction("/domain/cat.みんな").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_idn_utf8.txt"));
}
@Test
void testRun_wickedLineFeedForgeryInDatabase_crlfSubstitutedWithSpace() {
Contact trl =
FullFieldsTestEntityHelper.makeContact("5372808-TRL", "Eric Schmidt", "bog@cat.みんな");
trl =
persistResource(
trl.asBuilder()
.setInternationalizedPostalInfo(
trl.getInternationalizedPostalInfo()
.asBuilder()
.setOrg("Galactic\r\nEmpire")
.build())
.build());
persistResource(
makeDomain(
"cat.みんな",
trl,
trl,
trl,
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.みんな", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.cat.みんな", "bad:f00d:cafe::15:beef")),
persistResource(
makeRegistrar("example", "Example Registrar", Registrar.State.ACTIVE))));
newWhoisHttpAction("/domain/cat.みんな").run();
assertThat(response.getPayload()).contains("Galactic Empire");
}
@Test
void testRun_domainOnly_works() {
persistResource(
makeDomain(
"cat.みんな",
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-ERL", "(◕‿◕)", "lol@cat.みんな")),
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-IRL", "Operator", "BOFH@cat.みんな")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-TRL", "Eric Schmidt", "bog@cat.みんな")),
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.みんな", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.cat.みんな", "bad:f00d:cafe::15:beef")),
persistResource(
makeRegistrar("example", "Example Registrar", Registrar.State.ACTIVE))));
newWhoisHttpAction("cat.みんな").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).contains("Domain Name: cat.みんな\r\n");
}
@Test
void testRun_domainQuery_registeredDomainUnaffectedByBsa() {
persistResource(
Tld.get("lol")
.asBuilder()
.setBsaEnrollStartTime(Optional.of(clock.nowUtc().minusDays(1)))
.build());
persistBsaLabel("cat");
Registrar registrar = persistResource(makeRegistrar("evilregistrar", "Yes Virginia", ACTIVE));
persistResource(
makeDomain(
"cat.lol",
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-ERL", "Goblin Market", "lol@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-IRL", "Santa Claus", "BOFH@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-TRL", "The Raven", "bog@cat.lol")),
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.cat.lol", "bad:f00d:cafe::15:beef")),
registrar));
persistResources(makeRegistrarPocs(registrar));
newWhoisHttpAction("domain cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_domain.txt"));
}
@Test
void testRun_domainQuery_unregisteredDomainShowBsaMessage() {
persistResource(
Tld.get("lol")
.asBuilder()
.setBsaEnrollStartTime(Optional.of(clock.nowUtc().minusDays(1)))
.build());
persistBsaLabel("cat");
newWhoisHttpAction("domain cat.lol\r\n").run();
assertThat(response.getStatus()).isEqualTo(404);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_domain_blocked_by_bsa.txt"));
}
@Test
void testRun_hostnameOnly_works() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.みんな", "1.2.3.4"));
newWhoisHttpAction("ns1.cat.みんな").run();
assertThat(response.getPayload()).contains("Server Name: ns1.cat.みんな\r\n");
}
@Test
void testRun_domainQueryPunycode_works() {
Registrar registrar = persistResource(makeRegistrar(
"evilregistrar", "Yes Virginia", Registrar.State.ACTIVE));
persistResource(
makeDomain(
"cat.みんな",
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-ERL", "(◕‿◕)", "lol@cat.みんな")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-IRL", "Santa Claus", "BOFH@cat.みんな")),
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-TRL", "The Raven", "bog@cat.みんな")),
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.みんな", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.cat.みんな", "bad:f00d:cafe::15:beef")),
registrar));
persistResources(makeRegistrarPocs(registrar));
newWhoisHttpAction("/domain/cat.xn--q9jyb4c").run();
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_idn_utf8.txt"));
}
@Test
void testRun_nameserverQuery_works() {
persistResource(loadRegistrar("TheRegistrar").asBuilder().setUrl("http://my.fake.url").build());
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
newWhoisHttpAction("/nameserver/ns1.cat.lol").run();
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_nameserver.txt"));
}
// todo (b/27378695): reenable or delete this test
@Disabled
@Test
void testRun_nameserverQueryInTestTld_notFound() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
newWhoisHttpAction("/nameserver/ns1.cat.lol").run();
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_nameserver.txt"));
}
@Test
void testRun_lastUpdateTimestamp_isPresentInResponse() {
clock.setTo(DateTime.parse("2020-07-12T23:52:43Z"));
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
newWhoisHttpAction("/nameserver/ns1.cat.lol").run();
assertThat(response.getPayload())
.contains(">>> Last update of WHOIS database: 2020-07-12T23:52:43Z <<<");
}
@Test
void testRun_nameserverQueryIdn_works() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.みんな", "1.2.3.4"));
newWhoisHttpAction("/nameserver/ns1.cat.みんな").run();
assertThat(response.getPayload()).contains("ns1.cat.みんな");
assertThat(response.getPayload()).contains("1.2.3.4");
}
@Test
void testRun_nameserverQueryPunycode_works() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.みんな", "1.2.3.4"));
newWhoisHttpAction("/nameserver/ns1.cat.xn--q9jyb4c").run();
assertThat(response.getPayload()).contains("ns1.cat.みんな");
assertThat(response.getPayload()).contains("1.2.3.4");
}
@Test
void testRun_trailingSlashInPath_getsIgnored() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.みんな", "1.2.3.4"));
newWhoisHttpAction("/nameserver/ns1.cat.xn--q9jyb4c/").run();
assertThat(response.getPayload()).contains("ns1.cat.みんな");
assertThat(response.getPayload()).contains("1.2.3.4");
}
@Test
void testRun_uppercaseDomain_ignoresCasing() {
persistResource(
makeDomain(
"cat.lol",
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-ERL", "Peter Murphy", "lol@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-IRL", "Operator", "BOFH@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-TRL", "Eric Schmidt", "bog@cat.lol")),
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.cat.lol", "bad:f00d:cafe::15:beef")),
persistResource(
makeRegistrar("example", "Example Registrar", Registrar.State.ACTIVE))));
newWhoisHttpAction("/domain/cat.LOL").run();
assertThat(response.getPayload()).contains("Domain Name: cat.lol\r\n");
}
@Test
void testRun_hairyPath_getsDecoded() {
persistResource(
makeDomain(
"cat.lol",
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-ERL", "Peter Murphy", "lol@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact("5372808-IRL", "Operator", "BOFH@cat.lol")),
persistResource(
FullFieldsTestEntityHelper.makeContact(
"5372808-TRL", "Eric Schmidt", "bog@cat.lol")),
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4")),
persistResource(
FullFieldsTestEntityHelper.makeHost("ns2.cat.lol", "bad:f00d:cafe::15:beef")),
persistResource(
makeRegistrar("example", "Example Registrar", Registrar.State.ACTIVE))));
// python -c "print ''.join('%' + hex(ord(c))[2:] for c in 'cat.lol')"
newWhoisHttpAction("/domain/%63%61%74%2e%6c%6f%6c").run();
assertThat(response.getPayload()).contains("Domain Name: cat.lol\r\n");
}
@Test
void testRun_registrarLookup_works() {
Registrar registrar = persistResource(
makeRegistrar("example", "Example Registrar, Inc.", Registrar.State.ACTIVE));
persistResources(makeRegistrarPocs(registrar));
// Notice the partial search without "inc".
newWhoisHttpAction("/registrar/Example%20Registrar").run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_registrar.txt"));
}
@Test
void testRun_registrarLookupInPendingState_returnsNotFound() {
Registrar registrar = persistResource(
makeRegistrar("example", "Example Registrar, Inc.", Registrar.State.PENDING));
persistResources(makeRegistrarPocs(registrar));
newWhoisHttpAction("/registrar/Example%20Registrar,%20Inc.").run();
assertThat(response.getStatus()).isEqualTo(404);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_registrar_not_found.txt"));
}
@Test
void testRun_registrarLookupWithTestType_returnsNotFound() {
Registrar registrar = persistResource(
makeRegistrar("example", "Example Registrar, Inc.", Registrar.State.ACTIVE)
.asBuilder().setType(Registrar.Type.TEST).setIanaIdentifier(null).build());
persistResources(makeRegistrarPocs(registrar));
newWhoisHttpAction("/registrar/Example%20Registrar,%20Inc.").run();
assertThat(response.getStatus()).isEqualTo(404);
assertThat(response.getPayload()).isEqualTo(loadFile("whois_action_registrar_not_found.txt"));
}
@Test
void testRun_metricsLoggedForSuccessfulCommand() {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
WhoisHttpAction action = newWhoisHttpAction("/nameserver/ns1.cat.lol");
action.whoisMetrics = mock(WhoisMetrics.class);
action.run();
WhoisMetric expected =
WhoisMetric.builderForRequest(clock)
.setCommandName("NameserverLookupByHost")
.setNumResults(1)
.setStatus(SC_OK)
.build();
verify(action.whoisMetrics).recordWhoisMetric(eq(expected));
}
@Test
void testRun_metricsLoggedForUnsuccessfulCommand() {
WhoisHttpAction action = newWhoisHttpAction("nic.%u307F%u3093%u306A");
action.whoisMetrics = mock(WhoisMetrics.class);
action.run();
WhoisMetric expected =
WhoisMetric.builderForRequest(clock).setNumResults(0).setStatus(SC_BAD_REQUEST).build();
verify(action.whoisMetrics).recordWhoisMetric(eq(expected));
}
@Test
void testRun_metricsLoggedForInternalServerError() throws Exception {
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
WhoisHttpAction action = newWhoisHttpAction("ns1.cat.lol");
action.whoisReader = mock(WhoisReader.class);
when(action.whoisReader.readCommand(any(Reader.class), eq(false), any(DateTime.class)))
.thenThrow(new IOException("missing cat interface"));
action.whoisMetrics = mock(WhoisMetrics.class);
RuntimeException thrown = assertThrows(RuntimeException.class, action::run);
assertThat(thrown).hasCauseThat().hasMessageThat().isEqualTo("missing cat interface");
WhoisMetric expected =
WhoisMetric.builderForRequest(clock)
.setNumResults(0)
.setStatus(SC_INTERNAL_SERVER_ERROR)
.build();
verify(action.whoisMetrics).recordWhoisMetric(eq(expected));
}
}

View File

@@ -1,81 +0,0 @@
// 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.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.request.RequestModule;
import google.registry.testing.FullFieldsTestEntityHelper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for Dagger injection of the whois package. */
final class WhoisInjectionTest {
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private final HttpServletRequest req = mock(HttpServletRequest.class);
private final HttpServletResponse rsp = mock(HttpServletResponse.class);
private final StringWriter httpOutput = new StringWriter();
@BeforeEach
void beforeEach() throws Exception {
when(rsp.getWriter()).thenReturn(new PrintWriter(httpOutput));
}
@Test
void testWhoisAction_injectsAndWorks() throws Exception {
createTld("lol");
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
when(req.getReader()).thenReturn(new BufferedReader(new StringReader("ns1.cat.lol\r\n")));
DaggerWhoisTestComponent.builder()
.requestModule(new RequestModule(req, rsp))
.build()
.whoisAction()
.run();
verify(rsp).setStatus(200);
assertThat(httpOutput.toString()).contains("ns1.cat.lol");
}
@Test
void testWhoisHttpAction_injectsAndWorks() {
createTld("lol");
persistResource(FullFieldsTestEntityHelper.makeHost("ns1.cat.lol", "1.2.3.4"));
when(req.getRequestURI()).thenReturn("/whois/ns1.cat.lol");
DaggerWhoisTestComponent.builder()
.requestModule(new RequestModule(req, rsp))
.build()
.whoisHttpAction()
.run();
verify(rsp).setStatus(200);
assertThat(httpOutput.toString()).contains("ns1.cat.lol");
}
}

View File

@@ -1,423 +0,0 @@
// 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.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.createTlds;
import static google.registry.testing.LogsSubject.assertAboutLogs;
import static org.junit.jupiter.api.Assertions.assertThrows;
import com.google.common.testing.TestLogHandler;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import google.registry.util.JdkLoggerConfig;
import java.io.StringReader;
import java.util.logging.Level;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link WhoisReader}. */
class WhoisReaderTest {
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private final FakeClock clock = new FakeClock();
private final TestLogHandler testLogHandler = new TestLogHandler();
@BeforeEach
void beforeEach() {
createTlds("tld", "xn--kgbechtv", "1.test");
JdkLoggerConfig.getConfig(WhoisReader.class).addHandler(testLogHandler);
}
@SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
<T> T readCommand(String commandStr) throws Exception {
return (T)
new WhoisReader(
WhoisCommandFactory.createCached(), "Please contact registrar", "Blocked by BSA")
.readCommand(new StringReader(commandStr), false, clock.nowUtc());
}
void assertLoadsExampleTld(String commandString) throws Exception {
DomainLookupCommand command = readCommand(commandString);
assertThat(command.domainName.toString()).isEqualTo("example.tld");
}
void assertLoadsIDN(String commandString) throws Exception {
DomainLookupCommand command = readCommand(commandString);
assertThat(command.domainName.toString()).isEqualTo("xn--mgbh0fb.xn--kgbechtv");
}
void assertLoadsExampleNs(String commandString) throws Exception {
NameserverLookupByHostCommand command = readCommand(commandString);
assertThat(command.hostName.toString()).isEqualTo("ns.example.tld");
}
void assertLoadsIDNNs(String commandString) throws Exception {
NameserverLookupByHostCommand command = readCommand(commandString);
assertThat(command.hostName.toString()).isEqualTo("ns.xn--mgbh0fb.xn--kgbechtv");
}
void assertNsLookup(String commandString, String expectedIpAddress) throws Exception {
assertThat(
this.<NameserverLookupByIpCommand>readCommand(commandString).ipAddress.getHostAddress())
.isEqualTo(expectedIpAddress);
}
void assertLoadsRegistrar(String commandString) throws Exception {
assertThat(this.<RegistrarLookupCommand>readCommand(commandString).registrarName)
.isEqualTo("Example Registrar, Inc.");
}
@Test
void testRegistrarLookupWithOneToken() throws Exception {
assertThat(this.<RegistrarLookupCommand>readCommand("Example").registrarName)
.isEqualTo("Example");
}
@Test
void testDomainLookupWithoutCRLF() throws Exception {
assertLoadsExampleTld("example.tld");
}
@Test
void testWhitespaceOnDomainLookupWithCommand() throws Exception {
assertLoadsExampleTld(" \t domain \t \t example.tld \r\n");
}
@Test
void testDomainLookup() throws Exception {
assertLoadsExampleTld("example.tld\r\n");
}
@Test
void testDomainLookupWithCommand() throws Exception {
assertLoadsExampleTld("domain example.tld\r\n");
}
@Test
void testCaseInsensitiveDomainLookup() throws Exception {
assertLoadsExampleTld("EXAMPLE.TLD\r\n");
}
@Test
void testCaseInsensitiveDomainLookupWithCommand() throws Exception {
assertLoadsExampleTld("DOMAIN EXAMPLE.TLD\r\n");
}
@Test
void testIDNULabelDomainLookup() throws Exception {
assertLoadsIDN("مثال.إختبار\r\n");
}
@Test
void testIDNULabelDomainLookupWithCommand() throws Exception {
assertLoadsIDN("domain مثال.إختبار\r\n");
}
@Test
void testIDNALabelDomainLookupWithCommand() throws Exception {
assertLoadsIDN("domain xn--mgbh0fb.xn--kgbechtv\r\n");
}
@Test
void testIDNALabelDomainLookup() throws Exception {
assertLoadsIDN("xn--mgbh0fb.xn--kgbechtv\r\n");
}
@Test
void testTooManyArgsDomainLookup() {
assertThrows(WhoisException.class, () -> readCommand("domain example.tld foo.bar"));
}
@Test
void testTooFewArgsDomainLookup() {
assertThrows(WhoisException.class, () -> readCommand("domain"));
}
@Test
void testIllegalArgDomainLookup() {
assertThrows(WhoisException.class, () -> readCommand("domain 1.1"));
}
@Test
void testNameserverLookupWithoutCRLF() throws Exception {
assertLoadsExampleNs("ns.example.tld");
}
@Test
void testWhitespaceOnNameserverLookupWithCommand() throws Exception {
assertLoadsExampleNs(" \t nameserver \t \t ns.example.tld \r\n");
}
@Test
void testNameserverLookup() throws Exception {
assertLoadsExampleNs("ns.example.tld\r\n");
}
@Test
void testDeepNameserverLookup() throws Exception {
NameserverLookupByHostCommand command = readCommand("ns.foo.bar.baz.example.tld\r\n");
assertThat(command.hostName.toString()).isEqualTo("ns.foo.bar.baz.example.tld");
assertThat(command.hostName.toString()).isEqualTo("ns.foo.bar.baz.example.tld");
}
@Test
void testNameserverLookupWithCommand() throws Exception {
assertLoadsExampleNs("nameserver ns.example.tld\r\n");
}
@Test
void testCaseInsensitiveNameserverLookup() throws Exception {
assertLoadsExampleNs("NS.EXAMPLE.TLD\r\n");
}
@Test
void testCaseInsensitiveNameserverLookupWithCommand() throws Exception {
assertLoadsExampleNs("NAMESERVER NS.EXAMPLE.TLD\r\n");
}
@Test
void testIDNULabelNameserverLookup() throws Exception {
assertLoadsIDNNs("ns.مثال.إختبار\r\n");
}
@Test
void testIDNULabelNameserverLookupWithCommand() throws Exception {
assertLoadsIDNNs("nameserver ns.مثال.إختبار\r\n");
}
@Test
void testIDNALabelNameserverLookupWithCommand() throws Exception {
assertLoadsIDNNs("nameserver ns.xn--mgbh0fb.xn--kgbechtv\r\n");
}
@Test
void testIDNALabelNameserverLookup() throws Exception {
assertLoadsIDNNs("ns.xn--mgbh0fb.xn--kgbechtv\r\n");
}
@Test
void testTooManyArgsNameserverLookup() {
assertThrows(WhoisException.class, () -> readCommand("nameserver ns.example.tld foo.bar"));
}
@Test
void testTooFewArgsNameserverLookup() {
assertThrows(WhoisException.class, () -> readCommand("nameserver"));
}
@Test
void testIllegalArgNameserverLookup() {
assertThrows(WhoisException.class, () -> readCommand("nameserver 1.1"));
}
@Test
void testRegistrarLookup() throws Exception {
assertLoadsRegistrar("registrar Example Registrar, Inc.");
}
@Test
void testRegistrarLookupCaseInsensitive() throws Exception {
assertLoadsRegistrar("REGISTRAR Example Registrar, Inc.");
}
@Test
void testRegistrarLookupWhitespace() throws Exception {
assertLoadsRegistrar(" \t registrar \t \tExample Registrar, Inc. ");
}
@Test
void testRegistrarLookupByDefault() throws Exception {
assertLoadsRegistrar("Example Registrar, Inc.");
}
@Test
void testRegistrarLookupOnTLD() throws Exception {
assertThat(this.<RegistrarLookupCommand>readCommand("com").registrarName).isEqualTo("com");
}
@Test
void testRegistrarLookupNoArgs() {
assertThrows(WhoisException.class, () -> readCommand("registrar"));
}
@Test
void testNameserverLookupByIp() throws Exception {
assertNsLookup("43.34.12.213", "43.34.12.213");
}
@Test
void testNameserverLookupByIpv6() throws Exception {
assertNsLookup("1080:0:0:0:8:800:200c:417a", "1080:0:0:0:8:800:200c:417a");
}
@Test
void testNameserverLookupByCompressedIpv6() throws Exception {
assertNsLookup("1080::8:800:200c:417a", "1080:0:0:0:8:800:200c:417a");
}
@Test
void testNameserverLookupByNoncanonicalIpv6() throws Exception {
assertNsLookup("1080:0:0:0:8:800:200C:417A", "1080:0:0:0:8:800:200c:417a");
}
@Test
void testNameserverLookupByBackwardsCompatibleIpv6() throws Exception {
assertNsLookup("::FFFF:129.144.52.38", "129.144.52.38");
}
@Test
void testNameserverLookupByIpWithCommand() throws Exception {
assertNsLookup("nameserver 43.34.12.213", "43.34.12.213");
}
@Test
void testNameserverLookupByIpv6WithCommand() throws Exception {
assertNsLookup("nameserver 1080:0:0:0:8:800:200C:417a", "1080:0:0:0:8:800:200c:417a");
}
@Test
void testNameserverLookupByIpCaseInsenstive() throws Exception {
assertNsLookup("NAMESERVER 43.34.12.213", "43.34.12.213");
}
@Test
void testNameserverLookupByIpWhitespace() throws Exception {
assertNsLookup(" \t\t NAMESERVER \t 43.34.12.213 \r\n", "43.34.12.213");
}
@Test
void testNameserverLookupByIpTooManyArgs() {
assertThrows(WhoisException.class, () -> readCommand("nameserver 43.34.12.213 43.34.12.213"));
}
@Test
void testMultilevelDomainLookup() throws Exception {
this.<DomainLookupCommand>readCommand("example.1.test");
}
@Test
void testMultilevelNameserverLookup() throws Exception {
this.<NameserverLookupByHostCommand>readCommand("ns.example.1.test");
}
@Test
void testDeepMultilevelNameserverLookup() throws Exception {
this.<NameserverLookupByHostCommand>readCommand("ns.corp.example.1.test");
}
@Test
void testUnconfiguredTld() throws Exception {
this.<RegistrarLookupCommand>readCommand("example.test");
this.<RegistrarLookupCommand>readCommand("1.example.test");
this.<RegistrarLookupCommand>readCommand("ns.example.2.test");
this.<RegistrarLookupCommand>readCommand("test");
this.<RegistrarLookupCommand>readCommand("tld");
}
@Test
void testNoArgs() {
assertThrows(WhoisException.class, () -> readCommand(""));
}
@Test
void testLogsDomainLookupCommand() throws Exception {
readCommand("domain example.tld");
assertAboutLogs()
.that(testLogHandler)
.hasLogAtLevelWithMessage(
Level.INFO, "Attempting domain lookup command using domain name 'example.tld'.");
}
@Test
void testLogsNameserverLookupCommandWithIpAddress() throws Exception {
readCommand("nameserver 43.34.12.213");
assertAboutLogs()
.that(testLogHandler)
.hasLogAtLevelWithMessage(
Level.INFO,
"Attempting nameserver lookup command using 43.34.12.213 as an IP address.");
}
@Test
void testLogsNameserverLookupCommandWithHostname() throws Exception {
readCommand("nameserver ns.example.tld");
assertAboutLogs()
.that(testLogHandler)
.hasLogAtLevelWithMessage(
Level.INFO, "Attempting nameserver lookup command using ns.example.tld as a hostname.");
}
@Test
void testLogsRegistrarLookupCommand() throws Exception {
readCommand("registrar Example Registrar, Inc.");
assertAboutLogs()
.that(testLogHandler)
.hasLogAtLevelWithMessage(
Level.INFO,
"Attempting registrar lookup command using registrar Example Registrar, Inc.");
}
@Test
void testLogsSingleArgumentNameserverLookupUsingIpAddress() throws Exception {
readCommand("43.34.12.213");
assertAboutLogs()
.that(testLogHandler)
.hasLogAtLevelWithMessage(
Level.INFO, "Attempting nameserver lookup using 43.34.12.213 as an IP address.");
}
@Test
void testLogsSingleArgumentRegistrarLookup() throws Exception {
readCommand("test");
assertAboutLogs()
.that(testLogHandler)
.hasLogAtLevelWithMessage(
Level.INFO, "Attempting registrar lookup using test as a registrar.");
}
@Test
void testLogsSingleArgumentDomainLookup() throws Exception {
readCommand("example.tld");
assertAboutLogs()
.that(testLogHandler)
.hasLogAtLevelWithMessage(
Level.INFO, "Attempting domain lookup using example.tld as a domain name.");
}
@Test
void testLogsSingleArgumentNameserverLookupUsingHostname() throws Exception {
readCommand("ns.example.tld");
assertAboutLogs()
.that(testLogHandler)
.hasLogAtLevelWithMessage(
Level.INFO, "Attempting nameserver lookup using ns.example.tld as a hostname.");
}
@Test
void testLogsMultipleArgumentsButNoParticularCommand() throws Exception {
readCommand("Example Registrar, Inc.");
assertAboutLogs()
.that(testLogHandler)
.hasLogAtLevelWithMessage(
Level.INFO,
"Attempting registrar lookup employing Example Registrar, Inc. as a registrar.");
}
}

View File

@@ -1,35 +0,0 @@
// 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 dagger.Component;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.request.RequestModule;
import google.registry.util.UtilsModule;
import jakarta.inject.Singleton;
@Singleton
@Component(
modules = {
ConfigModule.class,
RequestModule.class,
UtilsModule.class,
WhoisModule.class,
})
interface WhoisTestComponent {
WhoisHttpAction whoisHttpAction();
WhoisAction whoisAction();
}

View File

@@ -1,29 +0,0 @@
// 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 google.registry.testing.TestDataHelper;
/** Test helper methods for the whois package. */
final class WhoisTestData {
/**
* Loads test data from file in {@code testdata/} directory, "fixing" newlines to have the ending
* that WHOIS requires.
*/
static String loadFile(String filename) {
return TestDataHelper.loadFile(WhoisTestData.class, filename).replaceAll("\r?\n", "\r\n");
}
}

View File

@@ -1,5 +1,4 @@
SERVICE PATH CLASS METHODS OK MIN USER_POLICY
PUBAPI /_dr/whois WhoisAction POST n APP ADMIN
PUBAPI /check CheckApiAction GET n NONE PUBLIC
PUBAPI /rdap/ RdapEmptyAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/autnum/(*) RdapAutnumAction GET,HEAD n NONE PUBLIC
@@ -12,4 +11,3 @@ PUBAPI /rdap/ip/(*) RdapIpAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/nameserver/(*) RdapNameserverAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/nameservers RdapNameserverSearchAction GET,HEAD n NONE PUBLIC
PUBAPI /ready/pubapi ReadinessProbeActionPubApi GET n NONE PUBLIC
PUBAPI /whois/(*) WhoisHttpAction GET n NONE PUBLIC

View File

@@ -56,7 +56,6 @@ BACKEND /_dr/task/tmchSmdrl TmchSmdrlAction
BACKEND /_dr/task/updateRegistrarRdapBaseUrls UpdateRegistrarRdapBaseUrlsAction GET y APP ADMIN
BACKEND /_dr/task/uploadBsaUnavailableNames UploadBsaUnavailableDomainsAction GET,POST n APP ADMIN
BACKEND /_dr/task/wipeOutContactHistoryPii WipeOutContactHistoryPiiAction GET n APP ADMIN
PUBAPI /_dr/whois WhoisAction POST n APP ADMIN
PUBAPI /check CheckApiAction GET n NONE PUBLIC
PUBAPI /rdap/ RdapEmptyAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/autnum/(*) RdapAutnumAction GET,HEAD n NONE PUBLIC
@@ -69,7 +68,6 @@ PUBAPI /rdap/ip/(*) RdapIpAction
PUBAPI /rdap/nameserver/(*) RdapNameserverAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/nameservers RdapNameserverSearchAction GET,HEAD n NONE PUBLIC
PUBAPI /ready/pubapi ReadinessProbeActionPubApi GET n NONE PUBLIC
PUBAPI /whois/(*) WhoisHttpAction GET n NONE PUBLIC
CONSOLE /console-api/bulk-domain ConsoleBulkDomainAction POST n USER PUBLIC
CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC
CONSOLE /console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC

View File

@@ -1,59 +0,0 @@
Domain Name: cat.lol
Registry Domain ID: 9-LOL
Registrar WHOIS Server: whois.example.com
Registrar URL: http://my.fake.url
Updated Date: 2009-05-29T20:13:00Z
Creation Date: 2000-10-08T00:45:00Z
Registry Expiry Date: 2110-10-08T00:44:59Z
Registrar: Yes Virginia
Registrar IANA ID: 1
Registrar Abuse Contact Email: jakedoe@example.com
Registrar Abuse Contact Phone: +1.2125551216
Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited
Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Domain Status: serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited
Registry Registrant ID: REDACTED FOR PRIVACY
Registrant Name: REDACTED FOR PRIVACY
Registrant Organization: GOOGLE INCORPORATED <script>
Registrant Street: REDACTED FOR PRIVACY
Registrant City: REDACTED FOR PRIVACY
Registrant State/Province: BM
Registrant Postal Code: REDACTED FOR PRIVACY
Registrant Country: US
Registrant Phone: REDACTED FOR PRIVACY
Registrant Fax: REDACTED FOR PRIVACY
Registrant Email: Please contact registrar
Registry Admin ID: REDACTED FOR PRIVACY
Admin Name: REDACTED FOR PRIVACY
Admin Organization: REDACTED FOR PRIVACY
Admin Street: REDACTED FOR PRIVACY
Admin City: REDACTED FOR PRIVACY
Admin State/Province: REDACTED FOR PRIVACY
Admin Postal Code: REDACTED FOR PRIVACY
Admin Country: REDACTED FOR PRIVACY
Admin Phone: REDACTED FOR PRIVACY
Admin Fax: REDACTED FOR PRIVACY
Admin Email: Please contact registrar
Registry Tech ID: REDACTED FOR PRIVACY
Tech Name: REDACTED FOR PRIVACY
Tech Organization: REDACTED FOR PRIVACY
Tech Street: REDACTED FOR PRIVACY
Tech City: REDACTED FOR PRIVACY
Tech State/Province: REDACTED FOR PRIVACY
Tech Postal Code: REDACTED FOR PRIVACY
Tech Country: REDACTED FOR PRIVACY
Tech Phone: REDACTED FOR PRIVACY
Tech Fax: REDACTED FOR PRIVACY
Tech Email: Please contact registrar
Name Server: ns1.cat.lol
Name Server: ns2.cat.lol
DNSSEC: signedDelegation
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
>>> Last update of WHOIS database: 2009-06-29T20:13:00Z <<<
For more information on Whois status codes, please visit https://icann.org/epp
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,6 +0,0 @@
Blocked by BSA: cat.lol
>>> Last update of WHOIS database: 2009-06-29T20:13:00Z <<<
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,6 +0,0 @@
Domain not found.
>>> Last update of WHOIS database: 2009-06-29T20:13:00Z <<<
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,59 +0,0 @@
Domain Name: cat.xn--q9jyb4c
Registry Domain ID: 9-Q9JYB4C
Registrar WHOIS Server: whois.example.com
Registrar URL: http://my.fake.url
Updated Date: 2009-05-29T20:13:00Z
Creation Date: 2000-10-08T00:45:00Z
Registry Expiry Date: 2110-10-08T00:44:59Z
Registrar: Yes Virginia
Registrar IANA ID: 1
Registrar Abuse Contact Email: jakedoe@example.com
Registrar Abuse Contact Phone: +1.2125551216
Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited
Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Domain Status: serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited
Registry Registrant ID: REDACTED FOR PRIVACY
Registrant Name: REDACTED FOR PRIVACY
Registrant Organization: GOOGLE INCORPORATED <script>
Registrant Street: REDACTED FOR PRIVACY
Registrant City: REDACTED FOR PRIVACY
Registrant State/Province: BM
Registrant Postal Code: REDACTED FOR PRIVACY
Registrant Country: US
Registrant Phone: REDACTED FOR PRIVACY
Registrant Fax: REDACTED FOR PRIVACY
Registrant Email: Please contact registrar
Registry Admin ID: REDACTED FOR PRIVACY
Admin Name: REDACTED FOR PRIVACY
Admin Organization: REDACTED FOR PRIVACY
Admin Street: REDACTED FOR PRIVACY
Admin City: REDACTED FOR PRIVACY
Admin State/Province: REDACTED FOR PRIVACY
Admin Postal Code: REDACTED FOR PRIVACY
Admin Country: REDACTED FOR PRIVACY
Admin Phone: REDACTED FOR PRIVACY
Admin Fax: REDACTED FOR PRIVACY
Admin Email: Please contact registrar
Registry Tech ID: REDACTED FOR PRIVACY
Tech Name: REDACTED FOR PRIVACY
Tech Organization: REDACTED FOR PRIVACY
Tech Street: REDACTED FOR PRIVACY
Tech City: REDACTED FOR PRIVACY
Tech State/Province: REDACTED FOR PRIVACY
Tech Postal Code: REDACTED FOR PRIVACY
Tech Country: REDACTED FOR PRIVACY
Tech Phone: REDACTED FOR PRIVACY
Tech Fax: REDACTED FOR PRIVACY
Tech Email: Please contact registrar
Name Server: ns1.cat.xn--q9jyb4c
Name Server: ns2.cat.xn--q9jyb4c
DNSSEC: signedDelegation
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
>>> Last update of WHOIS database: 2009-06-29T20:13:00Z <<<
For more information on Whois status codes, please visit https://icann.org/epp
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,59 +0,0 @@
Domain Name: cat.みんな
Registry Domain ID: 9-Q9JYB4C
Registrar WHOIS Server: whois.example.com
Registrar URL: http://my.fake.url
Updated Date: 2009-05-29T20:13:00Z
Creation Date: 2000-10-08T00:45:00Z
Registry Expiry Date: 2110-10-08T00:44:59Z
Registrar: Yes Virginia
Registrar IANA ID: 1
Registrar Abuse Contact Email: jakedoe@example.com
Registrar Abuse Contact Phone: +1.2125551216
Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited
Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Domain Status: serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited
Registry Registrant ID: REDACTED FOR PRIVACY
Registrant Name: REDACTED FOR PRIVACY
Registrant Organization: GOOGLE INCORPORATED <script>
Registrant Street: REDACTED FOR PRIVACY
Registrant City: REDACTED FOR PRIVACY
Registrant State/Province: BM
Registrant Postal Code: REDACTED FOR PRIVACY
Registrant Country: US
Registrant Phone: REDACTED FOR PRIVACY
Registrant Fax: REDACTED FOR PRIVACY
Registrant Email: Please contact registrar
Registry Admin ID: REDACTED FOR PRIVACY
Admin Name: REDACTED FOR PRIVACY
Admin Organization: REDACTED FOR PRIVACY
Admin Street: REDACTED FOR PRIVACY
Admin City: REDACTED FOR PRIVACY
Admin State/Province: REDACTED FOR PRIVACY
Admin Postal Code: REDACTED FOR PRIVACY
Admin Country: REDACTED FOR PRIVACY
Admin Phone: REDACTED FOR PRIVACY
Admin Fax: REDACTED FOR PRIVACY
Admin Email: Please contact registrar
Registry Tech ID: REDACTED FOR PRIVACY
Tech Name: REDACTED FOR PRIVACY
Tech Organization: REDACTED FOR PRIVACY
Tech Street: REDACTED FOR PRIVACY
Tech City: REDACTED FOR PRIVACY
Tech State/Province: REDACTED FOR PRIVACY
Tech Postal Code: REDACTED FOR PRIVACY
Tech Country: REDACTED FOR PRIVACY
Tech Phone: REDACTED FOR PRIVACY
Tech Fax: REDACTED FOR PRIVACY
Tech Email: Please contact registrar
Name Server: ns1.cat.みんな
Name Server: ns2.cat.みんな
DNSSEC: signedDelegation
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
>>> Last update of WHOIS database: 2009-06-29T20:13:00Z <<<
For more information on Whois status codes, please visit https://icann.org/epp
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,6 +0,0 @@
No nameservers found.
>>> Last update of WHOIS database: 2009-06-29T20:13:00Z <<<
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,6 +0,0 @@
Malformed path query.
>>> Last update of WHOIS database: 2009-06-29T20:13:00Z <<<
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,10 +0,0 @@
Server Name: ns1.cat.lol
IP Address: 1.2.3.4
Registrar: The Registrar
Registrar WHOIS Server: whois.nic.fakewhois.example
Registrar URL: http://my.fake.url
>>> Last update of WHOIS database: 2009-06-29T20:13:00Z <<<
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,6 +0,0 @@
Nameserver not found.
>>> Last update of WHOIS database: 2009-06-29T20:13:00Z <<<
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,6 +0,0 @@
No WHOIS command specified.
>>> Last update of WHOIS database: 2009-06-29T20:13:00Z <<<
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,24 +0,0 @@
Registrar: Example Registrar, Inc.
Street: 123 Example Boulevard <script>
City: Williamsburg <script>
State/Province: NY
Postal Code: 11211
Country: US
Phone Number: +1.2125551212
Fax Number: +1.2125551213
Email: contact-us@example.com
Registrar WHOIS Server: whois.example.com
Registrar URL: http://my.fake.url
Admin Contact: Jane Doe
Phone Number: +1.2125551215
Fax Number: +1.2125551216
Email: janedoe@example.com
Technical Contact: John Doe
Phone Number: +1.2125551213
Fax Number: +1.2125551213
Email: johndoe@example.com
>>> Last update of WHOIS database: 2009-06-29T20:13:00Z <<<
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,6 +0,0 @@
No registrar found.
>>> Last update of WHOIS database: 2009-06-29T20:13:00Z <<<
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,59 +0,0 @@
Domain Name: cat.lol
Registry Domain ID: 9-LOL
Registrar WHOIS Server: whois.example.com
Registrar URL: http://my.fake.url
Updated Date: 2010-03-06T00:00:00Z
Creation Date: 2000-10-08T00:45:00Z
Registry Expiry Date: 2020-03-01T00:00:00Z
Registrar: Yes Virginia
Registrar IANA ID: 1
Registrar Abuse Contact Email: jakedoe@example.com
Registrar Abuse Contact Phone: +1.2125551216
Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited
Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Domain Status: serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited
Registry Registrant ID: REDACTED FOR PRIVACY
Registrant Name: REDACTED FOR PRIVACY
Registrant Organization: GOOGLE INCORPORATED <script>
Registrant Street: REDACTED FOR PRIVACY
Registrant City: REDACTED FOR PRIVACY
Registrant State/Province: BM
Registrant Postal Code: REDACTED FOR PRIVACY
Registrant Country: US
Registrant Phone: REDACTED FOR PRIVACY
Registrant Fax: REDACTED FOR PRIVACY
Registrant Email: Please contact registrar
Registry Admin ID: REDACTED FOR PRIVACY
Admin Name: REDACTED FOR PRIVACY
Admin Organization: REDACTED FOR PRIVACY
Admin Street: REDACTED FOR PRIVACY
Admin City: REDACTED FOR PRIVACY
Admin State/Province: REDACTED FOR PRIVACY
Admin Postal Code: REDACTED FOR PRIVACY
Admin Country: REDACTED FOR PRIVACY
Admin Phone: REDACTED FOR PRIVACY
Admin Fax: REDACTED FOR PRIVACY
Admin Email: Please contact registrar
Registry Tech ID: REDACTED FOR PRIVACY
Tech Name: REDACTED FOR PRIVACY
Tech Organization: REDACTED FOR PRIVACY
Tech Street: REDACTED FOR PRIVACY
Tech City: REDACTED FOR PRIVACY
Tech State/Province: REDACTED FOR PRIVACY
Tech Postal Code: REDACTED FOR PRIVACY
Tech Country: REDACTED FOR PRIVACY
Tech Phone: REDACTED FOR PRIVACY
Tech Fax: REDACTED FOR PRIVACY
Tech Email: Please contact registrar
Name Server: ns1.cat.lol
Name Server: ns2.cat.lol
DNSSEC: signedDelegation
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
>>> Last update of WHOIS database: 2011-01-01T00:00:00Z <<<
For more information on Whois status codes, please visit https://icann.org/epp
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,66 +0,0 @@
Domain Name: example.tld
Registry Domain ID: 3-TLD
Registrar WHOIS Server: whois.nic.fakewhois.example
Registrar URL: http://my.fake.url
Updated Date: 2009-05-29T20:13:00Z
Creation Date: 2000-10-08T00:45:00Z
Registry Expiry Date: 2010-10-08T00:44:59Z
Registrar: New Registrar
Registrar IANA ID: 5555555
Registrar Abuse Contact Email: jakedoe@theregistrar.com
Registrar Abuse Contact Phone: +1.2125551216
Domain Status: addPeriod https://icann.org/epp#addPeriod
Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited
Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Domain Status: serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited
Domain Status: transferPeriod https://icann.org/epp#transferPeriod
Registry Registrant ID: REDACTED FOR PRIVACY
Registrant Name: REDACTED FOR PRIVACY
Registrant Organization: Tom & Jerry Corp.
Registrant Street: REDACTED FOR PRIVACY
Registrant City: REDACTED FOR PRIVACY
Registrant State/Province: AP
Registrant Postal Code: REDACTED FOR PRIVACY
Registrant Country: EX
Registrant Phone: REDACTED FOR PRIVACY
Registrant Phone Ext: REDACTED FOR PRIVACY
Registrant Fax: REDACTED FOR PRIVACY
Registrant Fax Ext: REDACTED FOR PRIVACY
Registrant Email: Please contact registrar
Registry Admin ID: REDACTED FOR PRIVACY
Admin Name: REDACTED FOR PRIVACY
Admin Organization: REDACTED FOR PRIVACY
Admin Street: REDACTED FOR PRIVACY
Admin City: REDACTED FOR PRIVACY
Admin State/Province: REDACTED FOR PRIVACY
Admin Postal Code: REDACTED FOR PRIVACY
Admin Country: REDACTED FOR PRIVACY
Admin Phone: REDACTED FOR PRIVACY
Admin Phone Ext: REDACTED FOR PRIVACY
Admin Fax: REDACTED FOR PRIVACY
Admin Email: Please contact registrar
Registry Tech ID: REDACTED FOR PRIVACY
Tech Name: REDACTED FOR PRIVACY
Tech Organization: REDACTED FOR PRIVACY
Tech Street: REDACTED FOR PRIVACY
Tech City: REDACTED FOR PRIVACY
Tech State/Province: REDACTED FOR PRIVACY
Tech Postal Code: REDACTED FOR PRIVACY
Tech Country: REDACTED FOR PRIVACY
Tech Phone: REDACTED FOR PRIVACY
Tech Phone Ext: REDACTED FOR PRIVACY
Tech Fax: REDACTED FOR PRIVACY
Tech Fax Ext: REDACTED FOR PRIVACY
Tech Email: Please contact registrar
Name Server: ns01.exampleregistrar.tld
Name Server: ns02.exampleregistrar.tld
DNSSEC: signedDelegation
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
>>> Last update of WHOIS database: 2009-05-29T20:15:00Z <<<
For more information on Whois status codes, please visit https://icann.org/epp
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,66 +0,0 @@
Domain Name: example.tld
Registry Domain ID: 3-TLD
Registrar WHOIS Server: whois.nic.fakewhois.example
Registrar URL: http://my.fake.url
Updated Date: 2009-05-29T20:13:00Z
Creation Date: 2000-10-08T00:45:00Z
Registry Expiry Date: 2010-10-08T00:44:59Z
Registrar: New Registrar
Registrar IANA ID: 5555555
Registrar Abuse Contact Email: jakedoe@theregistrar.com
Registrar Abuse Contact Phone: +1.2125551216
Domain Status: addPeriod https://icann.org/epp#addPeriod
Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited
Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Domain Status: serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited
Domain Status: transferPeriod https://icann.org/epp#transferPeriod
Registry Registrant ID: 4-ROID
Registrant Name: EXAMPLE REGISTRANT
Registrant Organization: Tom & Jerry Corp.
Registrant Street: 123 EXAMPLE STREET
Registrant City: ANYTOWN
Registrant State/Province: AP
Registrant Postal Code: A1A1A1
Registrant Country: EX
Registrant Phone: +1.5555551212
Registrant Phone Ext: 1234
Registrant Fax: +1.5555551213
Registrant Fax Ext: 4321
Registrant Email: EMAIL@EXAMPLE.tld
Registry Admin ID: 5-ROID
Admin Name: EXAMPLE REGISTRANT ADMINISTRATIVE
Admin Organization: EXAMPLE REGISTRANT ORGANIZATION
Admin Street: 123 EXAMPLE STREET
Admin City: ANYTOWN
Admin State/Province: AP
Admin Postal Code: A1A1A1
Admin Country: EX
Admin Phone: +1.5555551212
Admin Phone Ext: 1234
Admin Fax: +1.5555551213
Admin Email: EMAIL@EXAMPLE.tld
Registry Tech ID: 6-ROID
Tech Name: EXAMPLE REGISTRAR TECHNICAL
Tech Organization: EXAMPLE REGISTRAR LLC
Tech Street: 123 EXAMPLE STREET
Tech City: ANYTOWN
Tech State/Province: AP
Tech Postal Code: A1A1A1
Tech Country: EX
Tech Phone: +1.1235551234
Tech Phone Ext: 1234
Tech Fax: +1.5555551213
Tech Fax Ext: 93
Tech Email: EMAIL@EXAMPLE.tld
Name Server: ns01.exampleregistrar.tld
Name Server: ns02.exampleregistrar.tld
DNSSEC: signedDelegation
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
>>> Last update of WHOIS database: 2009-05-29T20:15:00Z <<<
For more information on Whois status codes, please visit https://icann.org/epp
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,28 +0,0 @@
Domain Name: example.tld
Registry Domain ID: 3-TLD
Registrar WHOIS Server: whois.nic.fakewhois.example
Registrar URL: http://my.fake.url
Updated Date: 2009-05-29T20:13:00Z
Creation Date: 2000-10-08T00:45:00Z
Registry Expiry Date: 2010-10-08T00:44:59Z
Registrar: New Registrar
Registrar IANA ID: 5555555
Registrar Abuse Contact Email: jakedoe@theregistrar.com
Registrar Abuse Contact Phone: +1.2125551216
Domain Status: addPeriod https://icann.org/epp#addPeriod
Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited
Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Domain Status: serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited
Domain Status: transferPeriod https://icann.org/epp#transferPeriod
Name Server: ns01.exampleregistrar.tld
Name Server: ns02.exampleregistrar.tld
DNSSEC: signedDelegation
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
>>> Last update of WHOIS database: 2009-05-29T20:15:00Z <<<
For more information on Whois status codes, please visit https://icann.org/epp
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,53 +0,0 @@
Domain Name: example.tld
Registry Domain ID: 3-TLD
Registrar WHOIS Server: whois.nic.fakewhois.example
Registrar URL: http://my.fake.url
Updated Date: 2009-05-29T20:13:00Z
Creation Date: 2000-10-08T00:45:00Z
Registry Expiry Date: 2010-10-08T00:44:59Z
Registrar: New Registrar
Registrar IANA ID: 5555555
Registrar Abuse Contact Email: jakedoe@theregistrar.com
Registrar Abuse Contact Phone: +1.2125551216
Domain Status: addPeriod https://icann.org/epp#addPeriod
Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited
Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Domain Status: serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited
Domain Status: transferPeriod https://icann.org/epp#transferPeriod
Registry Admin ID: REDACTED FOR PRIVACY
Admin Name: REDACTED FOR PRIVACY
Admin Organization: REDACTED FOR PRIVACY
Admin Street: REDACTED FOR PRIVACY
Admin City: REDACTED FOR PRIVACY
Admin State/Province: REDACTED FOR PRIVACY
Admin Postal Code: REDACTED FOR PRIVACY
Admin Country: REDACTED FOR PRIVACY
Admin Phone: REDACTED FOR PRIVACY
Admin Phone Ext: REDACTED FOR PRIVACY
Admin Fax: REDACTED FOR PRIVACY
Admin Email: Please contact registrar
Registry Tech ID: REDACTED FOR PRIVACY
Tech Name: REDACTED FOR PRIVACY
Tech Organization: REDACTED FOR PRIVACY
Tech Street: REDACTED FOR PRIVACY
Tech City: REDACTED FOR PRIVACY
Tech State/Province: REDACTED FOR PRIVACY
Tech Postal Code: REDACTED FOR PRIVACY
Tech Country: REDACTED FOR PRIVACY
Tech Phone: REDACTED FOR PRIVACY
Tech Phone Ext: REDACTED FOR PRIVACY
Tech Fax: REDACTED FOR PRIVACY
Tech Fax Ext: REDACTED FOR PRIVACY
Tech Email: Please contact registrar
Name Server: ns01.exampleregistrar.tld
Name Server: ns02.exampleregistrar.tld
DNSSEC: signedDelegation
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
>>> Last update of WHOIS database: 2009-05-29T20:15:00Z <<<
For more information on Whois status codes, please visit https://icann.org/epp
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,64 +0,0 @@
Domain Name: example.tld
Registry Domain ID: 3-TLD
Registrar WHOIS Server: whois.nic.fakewhois.example
Registrar URL: http://my.fake.url
Updated Date: 2009-05-29T20:13:00Z
Creation Date: 2000-10-08T00:45:00Z
Registry Expiry Date: 2010-10-08T00:44:59Z
Registrar: New Registrar
Registrar IANA ID: 5555555
Registrar Abuse Contact Email:
Registrar Abuse Contact Phone:
Domain Status: addPeriod https://icann.org/epp#addPeriod
Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited
Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
Domain Status: serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited
Domain Status: transferPeriod https://icann.org/epp#transferPeriod
Registry Registrant ID: REDACTED FOR PRIVACY
Registrant Name: REDACTED FOR PRIVACY
Registrant Organization: Tom & Jerry Corp.
Registrant Street: REDACTED FOR PRIVACY
Registrant City: REDACTED FOR PRIVACY
Registrant State/Province: AP
Registrant Postal Code: REDACTED FOR PRIVACY
Registrant Country: EX
Registrant Phone: REDACTED FOR PRIVACY
Registrant Phone Ext: REDACTED FOR PRIVACY
Registrant Fax: REDACTED FOR PRIVACY
Registrant Fax Ext: REDACTED FOR PRIVACY
Registrant Email: Please contact registrar
Registry Admin ID: REDACTED FOR PRIVACY
Admin Name: REDACTED FOR PRIVACY
Admin Organization: REDACTED FOR PRIVACY
Admin Street: REDACTED FOR PRIVACY
Admin City: REDACTED FOR PRIVACY
Admin State/Province: REDACTED FOR PRIVACY
Admin Postal Code: REDACTED FOR PRIVACY
Admin Country: REDACTED FOR PRIVACY
Admin Phone: REDACTED FOR PRIVACY
Admin Phone Ext: REDACTED FOR PRIVACY
Admin Fax: REDACTED FOR PRIVACY
Admin Email: Please contact registrar
Registry Tech ID: REDACTED FOR PRIVACY
Tech Name: REDACTED FOR PRIVACY
Tech Organization: REDACTED FOR PRIVACY
Tech Street: REDACTED FOR PRIVACY
Tech City: REDACTED FOR PRIVACY
Tech State/Province: REDACTED FOR PRIVACY
Tech Postal Code: REDACTED FOR PRIVACY
Tech Country: REDACTED FOR PRIVACY
Tech Phone: REDACTED FOR PRIVACY
Tech Phone Ext: REDACTED FOR PRIVACY
Tech Fax: REDACTED FOR PRIVACY
Tech Fax Ext: REDACTED FOR PRIVACY
Tech Email: Please contact registrar
Name Server: ns01.exampleregistrar.tld
Name Server: ns02.exampleregistrar.tld
DNSSEC: signedDelegation
URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/
>>> Last update of WHOIS database: 2009-05-29T20:15:00Z <<<
For more information on Whois status codes, please visit https://icann.org/epp
Footer

View File

@@ -1,18 +0,0 @@
Server Name: ns1.example.tld
IP Address: 192.0.2.123
IP Address: 2001:db8::1
Registrar: Hänsel & Gretel Registrar, Inc.
Registrar WHOIS Server: whois.nic.fakewhois.example
Registrar URL: http://my.fake.url
Server Name: ns2.example.tld
IP Address: 192.0.2.123
IP Address: 2001:db8::1
Registrar: Hänsel & Gretel Registrar, Inc.
Registrar WHOIS Server: whois.nic.fakewhois.example
Registrar URL: http://my.fake.url
>>> Last update of WHOIS database: 2009-05-29T20:15:00Z <<<
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,11 +0,0 @@
Server Name: ns1.example.tld
IP Address: 192.0.2.123
IP Address: 2001:db8::1
Registrar: Hänsel & Gretel Registrar, Inc.
Registrar WHOIS Server: whois.nic.fakewhois.example
Registrar URL: http://my.fake.url
>>> Last update of WHOIS database: 2009-05-29T20:15:00Z <<<
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,28 +0,0 @@
Registrar: Example Registrar, Inc.
Street: 1234 Admiralty Way
City: Marina del Rey
State/Province: CA
Postal Code: 90292
Country: US
Phone Number: +1.3105551212
Fax Number: +1.3105551213
Email: registrar@example.tld
Registrar WHOIS Server: whois.example-registrar.tld
Registrar URL: http://my.fake.url
Admin Contact: Jane Registrar
Phone Number: +1.3105551214
Fax Number: +1.3105551213
Email: janeregistrar@example-registrar.tld
Admin Contact: Joe Registrar
Phone Number: +1.3105551213
Fax Number: +1.3105551213
Email: joeregistrar@example-registrar.tld
Technical Contact: Bonnie & Clyde
Phone Number: +1.3105551215
Fax Number: +1.3105551216
Email: johngeek@example-registrar.tld
>>> Last update of WHOIS database: 2009-05-29T20:15:00Z <<<
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -1,11 +0,0 @@
Server Name: ns1.zobo.tld
IP Address: 192.0.2.123
IP Address: 2001:db8::1
Registrar: The Registrar
Registrar WHOIS Server: whois.nic.fakewhois.example
Registrar URL: http://my.fake.url
>>> Last update of WHOIS database: 2009-05-29T20:15:00Z <<<
Doodle Disclaimer
I exist so that carriage return
in disclaimer can be tested.

View File

@@ -29,7 +29,6 @@ public class Prober {
public static void main(String[] args) {
// Obtains WebWhois Sequence provided by proberComponent
ImmutableSet<ProbingSequence> sequences = ImmutableSet.copyOf(proberComponent.sequences());
// Tells Sequences to start running

View File

@@ -20,7 +20,6 @@ import dagger.Provides;
import google.registry.monitoring.blackbox.connection.ProbingAction;
import google.registry.monitoring.blackbox.module.CertificateModule;
import google.registry.monitoring.blackbox.module.EppModule;
import google.registry.monitoring.blackbox.module.WebWhoisModule;
import google.registry.networking.handler.SslClientInitializer;
import google.registry.util.Clock;
import google.registry.util.SystemClock;
@@ -102,13 +101,10 @@ public class ProberModule {
@Component(
modules = {
ProberModule.class,
WebWhoisModule.class,
EppModule.class,
CertificateModule.class
})
public interface ProberComponent {
// Standard WebWhois sequence
Set<ProbingSequence> sequences();
}
}

View File

@@ -1,180 +0,0 @@
// Copyright 2019 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.monitoring.blackbox.handler;
import com.google.common.flogger.FluentLogger;
import google.registry.monitoring.blackbox.connection.ProbingAction;
import google.registry.monitoring.blackbox.connection.Protocol;
import google.registry.monitoring.blackbox.exception.ConnectionException;
import google.registry.monitoring.blackbox.exception.FailureException;
import google.registry.monitoring.blackbox.exception.UndeterminedStateException;
import google.registry.monitoring.blackbox.message.HttpRequestMessage;
import google.registry.monitoring.blackbox.message.HttpResponseMessage;
import google.registry.monitoring.blackbox.message.InboundMessageType;
import google.registry.monitoring.blackbox.module.WebWhoisModule.HttpWhoisProtocol;
import google.registry.monitoring.blackbox.module.WebWhoisModule.HttpsWhoisProtocol;
import google.registry.monitoring.blackbox.module.WebWhoisModule.WebWhoisProtocol;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpResponseStatus;
import jakarta.inject.Inject;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import org.joda.time.Duration;
/**
* Subclass of {@link ActionHandler} that deals with the WebWhois Sequence
*
* <p>Main purpose is to verify {@link HttpResponseMessage} received is valid. If the response
* implies a redirection it follows the redirection until either an Error Response is received, or
* {@link HttpResponseStatus#OK} is received
*/
public class WebWhoisActionHandler extends ActionHandler {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/* Dagger injected components necessary for redirect responses: */
/** {@link Bootstrap} necessary for remaking connection on redirect response. */
private final Bootstrap bootstrap;
/** {@link Protocol} for when redirected to http endpoint. */
private final Protocol httpWhoisProtocol;
/** {@link Protocol} for when redirected to https endpoint. */
private final Protocol httpsWhoisProtocol;
/** {@link HttpRequestMessage} that represents default GET message to be sent on redirect. */
private final HttpRequestMessage requestMessage;
@Inject
public WebWhoisActionHandler(
@WebWhoisProtocol Bootstrap bootstrap,
@HttpWhoisProtocol Protocol httpWhoisProtocol,
@HttpsWhoisProtocol Protocol httpsWhoisProtocol,
HttpRequestMessage requestMessage) {
this.bootstrap = bootstrap;
this.httpWhoisProtocol = httpWhoisProtocol;
this.httpsWhoisProtocol = httpsWhoisProtocol;
this.requestMessage = requestMessage;
}
/**
* After receiving {@link HttpResponseMessage}, either notes success and marks future as finished,
* notes an error in the received {@link URL} and throws a {@link ConnectionException}, received a
* response indicating a Failure, or receives a redirection response, where it follows the
* redirects until receiving one of the previous three responses.
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, InboundMessageType msg)
throws FailureException, UndeterminedStateException {
HttpResponseMessage response = (HttpResponseMessage) msg;
if (response.status().equals(HttpResponseStatus.OK)) {
logger.atInfo().log("Received Successful HttpResponseStatus");
logger.atInfo().log("Response Received: %s", response);
// On success, we always pass message to ActionHandler's channelRead0 method.
super.channelRead0(ctx, msg);
} else if (response.status().equals(HttpResponseStatus.MOVED_PERMANENTLY)
|| response.status().equals(HttpResponseStatus.FOUND)) {
// TODO - Fix checker to better determine when we have encountered a redirection response.
// Obtain url to be redirected to
URL url;
try {
url = new URI(response.headers().get("Location")).toURL();
} catch (MalformedURLException | URISyntaxException | IllegalArgumentException e) {
// in case of error, log it, and let ActionHandler's exceptionThrown method deal with it
throw new FailureException(
"Redirected Location was invalid. Given Location was: "
+ response.headers().get("Location"));
}
// From url, extract new host, port, and path
String newHost = url.getHost();
String newPath = url.getPath();
logger.atInfo().log(
String.format(
"Redirected to %s with host: %s, port: %d, and path: %s",
url, newHost, url.getDefaultPort(), newPath));
// Construct new Protocol to reflect redirected host, path, and port
Protocol newProtocol;
if (url.getProtocol().equals(httpWhoisProtocol.name())) {
newProtocol = httpWhoisProtocol;
} else if (url.getProtocol().equals(httpsWhoisProtocol.name())) {
newProtocol = httpsWhoisProtocol;
} else {
throw new FailureException(
"Redirection Location port was invalid. Given protocol name was: " + url.getProtocol());
}
// Obtain HttpRequestMessage with modified headers to reflect new host and path.
HttpRequestMessage httpRequest = requestMessage.modifyMessage(newHost, newPath);
// Create new probingAction that takes in the new Protocol and HttpRequestMessage with no
// delay
ProbingAction redirectedAction =
ProbingAction.builder()
.setBootstrap(bootstrap)
.setProtocol(newProtocol)
.setOutboundMessage(httpRequest)
.setDelay(Duration.ZERO)
.setHost(newHost)
.build();
// close this channel as we no longer need it
ChannelFuture future = ctx.close();
future.addListener(
f -> {
if (f.isSuccess()) {
logger.atInfo().log("Successfully closed connection.");
} else {
logger.atWarning().log("Channel was unsuccessfully closed.");
}
// Once channel is closed, establish new connection to redirected host, and repeat
// same actions
ChannelFuture secondFuture = redirectedAction.call();
// Once we have a successful call, set original ChannelPromise as success to tell
// ProbingStep we can move on
secondFuture.addListener(
f2 -> {
if (f2.isSuccess()) {
super.channelRead0(ctx, msg);
} else {
if (f2 instanceof FailureException) {
throw new FailureException(f2.cause());
} else {
throw new UndeterminedStateException(f2.cause());
}
}
});
});
} else {
// Add in metrics Handling that informs MetricsCollector the response was a FAILURE
logger.atWarning().log(String.format("Received unexpected response: %s", response.status()));
throw new FailureException("Response received from remote site was: " + response.status());
}
}
}

View File

@@ -1,55 +0,0 @@
// Copyright 2019 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.monitoring.blackbox.handler;
import google.registry.monitoring.blackbox.message.HttpRequestMessage;
import google.registry.monitoring.blackbox.message.HttpResponseMessage;
import google.registry.monitoring.blackbox.message.InboundMessageType;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.FullHttpResponse;
import jakarta.inject.Inject;
/**
* {@link io.netty.channel.ChannelHandler} that converts inbound {@link FullHttpResponse} to custom
* type {@link HttpResponseMessage} and retains {@link HttpRequestMessage} in case of reuse for
* redirection.
*/
public class WebWhoisMessageHandler extends ChannelDuplexHandler {
@Inject
public WebWhoisMessageHandler() {}
/** Retains {@link HttpRequestMessage} and calls super write method. */
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
throws Exception {
HttpRequestMessage request = (HttpRequestMessage) msg;
request.retain();
super.write(ctx, request, promise);
}
/**
* Converts {@link FullHttpResponse} to {@link HttpResponseMessage}, so it is an {@link
* InboundMessageType} instance.
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
FullHttpResponse originalResponse = (FullHttpResponse) msg;
HttpResponseMessage response = new HttpResponseMessage(originalResponse);
super.channelRead(ctx, response);
}
}

View File

@@ -1,227 +0,0 @@
// Copyright 2019 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.monitoring.blackbox.module;
import static google.registry.monitoring.blackbox.connection.ProbingAction.REMOTE_ADDRESS_KEY;
import static google.registry.monitoring.blackbox.connection.Protocol.PROTOCOL_KEY;
import static google.registry.networking.handler.SslClientInitializer.createSslClientInitializerWithSystemTrustStore;
import com.google.common.collect.ImmutableList;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
import google.registry.monitoring.blackbox.ProbingSequence;
import google.registry.monitoring.blackbox.ProbingStep;
import google.registry.monitoring.blackbox.connection.Protocol;
import google.registry.monitoring.blackbox.handler.WebWhoisActionHandler;
import google.registry.monitoring.blackbox.handler.WebWhoisMessageHandler;
import google.registry.monitoring.blackbox.message.HttpRequestMessage;
import google.registry.monitoring.blackbox.metric.MetricsCollector;
import google.registry.monitoring.blackbox.token.WebWhoisToken;
import google.registry.networking.handler.SslClientInitializer;
import google.registry.util.Clock;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.ssl.SslProvider;
import jakarta.inject.Provider;
import jakarta.inject.Qualifier;
import jakarta.inject.Singleton;
import org.joda.time.Duration;
/**
* A module that provides the components necessary for and the overall {@link ProbingSequence} to
* probe WebWHOIS.
*/
@Module
public class WebWhoisModule {
private static final String HTTP_PROTOCOL_NAME = "http";
private static final String HTTPS_PROTOCOL_NAME = "https";
private static final int HTTP_WHOIS_PORT = 80;
private static final int HTTPS_WHOIS_PORT = 443;
/** Standard length of messages used by Proxy. Equates to 0.5 MB. */
private static final int maximumMessageLengthBytes = 512 * 1024;
/** {@link Provides} only step used in WebWhois sequence. */
@Provides
@WebWhoisProtocol
static ProbingStep provideWebWhoisStep(
@HttpWhoisProtocol Protocol httpWhoisProtocol,
@WebWhoisProtocol Bootstrap bootstrap,
HttpRequestMessage messageTemplate,
Duration duration) {
return ProbingStep.builder()
.setProtocol(httpWhoisProtocol)
.setBootstrap(bootstrap)
.setMessageTemplate(messageTemplate)
.setDuration(duration)
.build();
}
/** {@link Provides} the {@link Protocol} that corresponds to http connection. */
@Singleton
@Provides
@HttpWhoisProtocol
static Protocol provideHttpWhoisProtocol(
@HttpWhoisProtocol int httpWhoisPort,
@HttpWhoisProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
return Protocol.builder()
.setName(HTTP_PROTOCOL_NAME)
.setPort(httpWhoisPort)
.setHandlerProviders(handlerProviders)
.setPersistentConnection(false)
.build();
}
/** {@link Provides} the {@link Protocol} that corresponds to https connection. */
@Singleton
@Provides
@HttpsWhoisProtocol
static Protocol provideHttpsWhoisProtocol(
@HttpsWhoisProtocol int httpsWhoisPort,
@HttpsWhoisProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
return Protocol.builder()
.setName(HTTPS_PROTOCOL_NAME)
.setPort(httpsWhoisPort)
.setHandlerProviders(handlerProviders)
.setPersistentConnection(false)
.build();
}
/**
* {@link Provides} the list of providers of {@link ChannelHandler}s that are used for http
* protocol.
*/
@Provides
@HttpWhoisProtocol
static ImmutableList<Provider<? extends ChannelHandler>> providerHttpWhoisHandlerProviders(
Provider<HttpClientCodec> httpClientCodecProvider,
Provider<HttpObjectAggregator> httpObjectAggregatorProvider,
Provider<WebWhoisMessageHandler> messageHandlerProvider,
Provider<WebWhoisActionHandler> webWhoisActionHandlerProvider) {
return ImmutableList.of(
httpClientCodecProvider,
httpObjectAggregatorProvider,
messageHandlerProvider,
webWhoisActionHandlerProvider);
}
/**
* {@link Provides} the list of providers of {@link ChannelHandler}s that are used for https
* protocol.
*/
@Provides
@HttpsWhoisProtocol
static ImmutableList<Provider<? extends ChannelHandler>> providerHttpsWhoisHandlerProviders(
@HttpsWhoisProtocol
Provider<SslClientInitializer<NioSocketChannel>> sslClientInitializerProvider,
Provider<HttpClientCodec> httpClientCodecProvider,
Provider<HttpObjectAggregator> httpObjectAggregatorProvider,
Provider<WebWhoisMessageHandler> messageHandlerProvider,
Provider<WebWhoisActionHandler> webWhoisActionHandlerProvider) {
return ImmutableList.of(
sslClientInitializerProvider,
httpClientCodecProvider,
httpObjectAggregatorProvider,
messageHandlerProvider,
webWhoisActionHandlerProvider);
}
@Provides
static HttpClientCodec provideHttpClientCodec() {
return new HttpClientCodec();
}
@Provides
static HttpObjectAggregator provideHttpObjectAggregator(@WebWhoisProtocol int maxContentLength) {
return new HttpObjectAggregator(maxContentLength);
}
/** {@link Provides} the {@link SslClientInitializer} used for the {@link HttpsWhoisProtocol}. */
@Provides
@HttpsWhoisProtocol
static SslClientInitializer<NioSocketChannel> provideSslClientInitializer(
SslProvider sslProvider) {
return createSslClientInitializerWithSystemTrustStore(
sslProvider,
channel -> channel.attr(REMOTE_ADDRESS_KEY).get(),
channel -> channel.attr(PROTOCOL_KEY).get().port());
}
/** {@link Provides} the {@link Bootstrap} used by the WebWhois sequence. */
@Singleton
@Provides
@WebWhoisProtocol
static Bootstrap provideBootstrap(
EventLoopGroup eventLoopGroup, Class<? extends Channel> channelClazz) {
return new Bootstrap().group(eventLoopGroup).channel(channelClazz);
}
/** {@link Provides} standard WebWhois sequence. */
@Provides
@Singleton
@IntoSet
ProbingSequence provideWebWhoisSequence(
@WebWhoisProtocol ProbingStep probingStep,
WebWhoisToken webWhoisToken,
MetricsCollector metrics,
Clock clock) {
return new ProbingSequence.Builder(webWhoisToken, metrics, clock).add(probingStep).build();
}
@Provides
@WebWhoisProtocol
int provideMaximumMessageLengthBytes() {
return maximumMessageLengthBytes;
}
/** {@link Provides} the list of top level domains to be probed */
@Singleton
@Provides
@WebWhoisProtocol
ImmutableList<String> provideTopLevelDomains() {
return ImmutableList.of("how", "soy", "xn--q9jyb4c");
}
@Provides
@HttpWhoisProtocol
int provideHttpWhoisPort() {
return HTTP_WHOIS_PORT;
}
@Provides
@HttpsWhoisProtocol
int provideHttpsWhoisPort() {
return HTTPS_WHOIS_PORT;
}
/** Dagger qualifier to provide HTTP whois protocol related handlers and other bindings. */
@Qualifier
public @interface HttpWhoisProtocol {}
/** Dagger qualifier to provide HTTPS whois protocol related handlers and other bindings. */
@Qualifier
public @interface HttpsWhoisProtocol {}
/** Dagger qualifier to provide any WebWhois related bindings. */
@Qualifier
public @interface WebWhoisProtocol {}
}

View File

@@ -1,73 +0,0 @@
// Copyright 2019 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.monitoring.blackbox.token;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.PeekingIterator;
import google.registry.monitoring.blackbox.exception.UndeterminedStateException;
import google.registry.monitoring.blackbox.message.OutboundMessageType;
import google.registry.monitoring.blackbox.module.WebWhoisModule.WebWhoisProtocol;
import jakarta.inject.Inject;
/**
* {@link Token} subtype designed for WebWhois sequence.
*
* <p>Between loops of a WebWhois sequence the only thing changing is the tld we are probing. As a
* result, we maintain the list of {@code topLevelDomains} and on each call to next, have our index
* looking at the next {@code topLevelDomain}.
*/
public class WebWhoisToken extends Token {
/** For each top level domain (tld), we probe "prefix.tld". */
private static final String PREFIX = "whois.nic.";
/** {@link PeekingIterator} over a cycle of all top level domains to be probed. */
private final PeekingIterator<String> tldCycleIterator;
@Inject
public WebWhoisToken(@WebWhoisProtocol ImmutableList<String> topLevelDomainsList) {
checkArgument(!topLevelDomainsList.isEmpty(), "topLevelDomainsList must not be empty.");
this.tldCycleIterator =
Iterators.peekingIterator(Iterables.cycle(topLevelDomainsList).iterator());
}
/** Moves on to next top level domain in {@code tldCycleIterator}. */
@Override
public WebWhoisToken next() {
tldCycleIterator.next();
return this;
}
/** Modifies message to reflect the new host coming from the new top level domain. */
@Override
public OutboundMessageType modifyMessage(OutboundMessageType original)
throws UndeterminedStateException {
return original.modifyMessage(host());
}
/**
* Returns host as the concatenation of fixed {@code prefix} and current value of {@code
* topLevelDomains}.
*/
@Override
public String host() {
return PREFIX + tldCycleIterator.peek();
}
}

View File

@@ -1,216 +0,0 @@
// Copyright 2019 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.monitoring.blackbox.handler;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.monitoring.blackbox.connection.ProbingAction.CONNECTION_FUTURE_KEY;
import static google.registry.monitoring.blackbox.connection.Protocol.PROTOCOL_KEY;
import static google.registry.monitoring.blackbox.util.WebWhoisUtils.makeHttpGetRequest;
import static google.registry.monitoring.blackbox.util.WebWhoisUtils.makeHttpResponse;
import static google.registry.monitoring.blackbox.util.WebWhoisUtils.makeRedirectResponse;
import com.google.common.collect.ImmutableList;
import google.registry.monitoring.blackbox.connection.Protocol;
import google.registry.monitoring.blackbox.exception.FailureException;
import google.registry.monitoring.blackbox.message.HttpRequestMessage;
import google.registry.monitoring.blackbox.message.HttpResponseMessage;
import google.registry.monitoring.blackbox.testserver.TestServer;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import jakarta.inject.Provider;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link WebWhoisActionHandler}.
*
* <p>Attempts to test how well {@link WebWhoisActionHandler} works when responding to all possible
* types of responses
*/
class WebWhoisActionHandlerTest {
private static final int HTTP_PORT = 80;
private static final String HTTP_REDIRECT = "http://";
private static final String TARGET_HOST = "whois.nic.tld";
private static final String DUMMY_URL = "__WILL_NOT_WORK__";
private final Protocol standardProtocol =
Protocol.builder()
.setHandlerProviders(
ImmutableList.of(() -> new WebWhoisActionHandler(null, null, null, null)))
.setName("http")
.setPersistentConnection(false)
.setPort(HTTP_PORT)
.build();
private EmbeddedChannel channel;
private ActionHandler actionHandler;
private Provider<? extends ChannelHandler> actionHandlerProvider;
private Protocol initialProtocol;
private HttpRequestMessage msg;
/** Creates default protocol with empty list of handlers and specified other inputs */
private Protocol createProtocol(String name, int port, boolean persistentConnection) {
return Protocol.builder()
.setName(name)
.setPort(port)
.setHandlerProviders(ImmutableList.of(actionHandlerProvider))
.setPersistentConnection(persistentConnection)
.build();
}
/** Initializes new WebWhoisActionHandler */
private void setupActionHandler(Bootstrap bootstrap, HttpRequestMessage messageTemplate) {
actionHandler =
new WebWhoisActionHandler(bootstrap, standardProtocol, standardProtocol, messageTemplate);
actionHandlerProvider = () -> actionHandler;
}
/** Sets up testing channel with requisite attributes */
private void setupChannel(Protocol protocol) {
channel = new EmbeddedChannel(actionHandler);
channel.attr(PROTOCOL_KEY).set(protocol);
channel.attr(CONNECTION_FUTURE_KEY).set(channel.newSucceededFuture());
}
private Bootstrap makeBootstrap(EventLoopGroup group) {
return new Bootstrap().group(group).channel(LocalChannel.class);
}
private void setup(String hostName, Bootstrap bootstrap, boolean persistentConnection) {
msg = new HttpRequestMessage(makeHttpGetRequest(hostName, ""));
setupActionHandler(bootstrap, msg);
initialProtocol = createProtocol("testProtocol", 0, persistentConnection);
}
@Test
void testBasic_responseOk() {
// setup
setup("", null, true);
setupChannel(initialProtocol);
// stores future
ChannelFuture future = actionHandler.getFinishedFuture();
channel.writeOutbound(msg);
FullHttpResponse response = new HttpResponseMessage(makeHttpResponse(HttpResponseStatus.OK));
// assesses that future listener isn't triggered yet.
assertThat(future.isDone()).isFalse();
channel.writeInbound(response);
// assesses that we successfully received good response and protocol is unchanged
assertThat(future.isSuccess()).isTrue();
}
@Test
void testBasic_responseFailure_badRequest() {
// setup
setup("", null, false);
setupChannel(initialProtocol);
// Stores future that informs when action is completed.
ChannelFuture future = actionHandler.getFinishedFuture();
channel.writeOutbound(msg);
FullHttpResponse response =
new HttpResponseMessage(makeHttpResponse(HttpResponseStatus.BAD_REQUEST));
// Assesses that future listener isn't triggered yet.
assertThat(future.isDone()).isFalse();
channel.writeInbound(response);
// Assesses that listener is triggered, but event is not success
assertThat(future.isDone()).isTrue();
assertThat(future.isSuccess()).isFalse();
// Ensures that we fail as a result of a FailureException.
assertThat(future.cause() instanceof FailureException).isTrue();
}
@Test
void testBasic_responseFailure_badURL() {
// setup
setup("", null, false);
setupChannel(initialProtocol);
// stores future
ChannelFuture future = actionHandler.getFinishedFuture();
channel.writeOutbound(msg);
FullHttpResponse response =
new HttpResponseMessage(
makeRedirectResponse(HttpResponseStatus.MOVED_PERMANENTLY, DUMMY_URL, true));
// assesses that future listener isn't triggered yet.
assertThat(future.isDone()).isFalse();
channel.writeInbound(response);
// assesses that listener is triggered, and event is success
assertThat(future.isDone()).isTrue();
assertThat(future.isSuccess()).isFalse();
// Ensures that we fail as a result of a FailureException.
assertThat(future.cause()).isInstanceOf(FailureException.class);
}
@Test
void testAdvanced_redirect() {
// Sets up EventLoopGroup with 1 thread to be blocking.
EventLoopGroup group = new NioEventLoopGroup(1);
// Sets up embedded channel.
setup("", makeBootstrap(group), false);
setupChannel(initialProtocol);
// Initializes LocalAddress with unique String.
LocalAddress address = new LocalAddress(TARGET_HOST);
// stores future
ChannelFuture future = actionHandler.getFinishedFuture();
channel.writeOutbound(msg);
// Path that we test WebWhoisActionHandler uses.
String path = "/test";
// Sets up the local server that the handler will be redirected to.
TestServer.webWhoisServer(group, address, "", TARGET_HOST, path);
FullHttpResponse response =
new HttpResponseMessage(
makeRedirectResponse(
HttpResponseStatus.MOVED_PERMANENTLY, HTTP_REDIRECT + TARGET_HOST + path, true));
// checks that future has not been set to successful or a failure
assertThat(future.isDone()).isFalse();
channel.writeInbound(response);
// makes sure old channel is shut down when attempting redirection
assertThat(channel.isActive()).isFalse();
// assesses that we successfully received good response and protocol is unchanged
assertThat(future.syncUninterruptibly().isSuccess()).isTrue();
}
}

View File

@@ -1,165 +0,0 @@
// Copyright 2019 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.monitoring.blackbox.testserver;
import static google.registry.monitoring.blackbox.util.WebWhoisUtils.makeHttpResponse;
import static google.registry.monitoring.blackbox.util.WebWhoisUtils.makeRedirectResponse;
import com.google.common.collect.ImmutableList;
import google.registry.monitoring.blackbox.exception.FailureException;
import google.registry.monitoring.blackbox.message.EppMessage;
import google.registry.monitoring.blackbox.message.HttpResponseMessage;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalChannel;
import io.netty.channel.local.LocalServerChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
/**
* Mock Server Superclass whose subclasses implement specific behaviors we expect blackbox server to
* perform
*/
public class TestServer {
public TestServer(LocalAddress localAddress, ImmutableList<? extends ChannelHandler> handlers) {
this(new NioEventLoopGroup(1), localAddress, handlers);
}
private TestServer(
EventLoopGroup eventLoopGroup,
LocalAddress localAddress,
ImmutableList<? extends ChannelHandler> handlers) {
// Creates ChannelInitializer with handlers specified
ChannelInitializer<LocalChannel> serverInitializer =
new ChannelInitializer<>() {
@Override
protected void initChannel(LocalChannel ch) {
for (ChannelHandler handler : handlers) {
ch.pipeline().addLast(handler);
}
}
};
// Sets up serverBootstrap with specified initializer, eventLoopGroup, and using
// LocalServerChannel class
ServerBootstrap serverBootstrap =
new ServerBootstrap()
.group(eventLoopGroup)
.channel(LocalServerChannel.class)
.childHandler(serverInitializer);
ChannelFuture unusedFuture = serverBootstrap.bind(localAddress).syncUninterruptibly();
}
public static TestServer webWhoisServer(
EventLoopGroup eventLoopGroup,
LocalAddress localAddress,
String redirectInput,
String destinationInput,
String destinationPath) {
return new TestServer(
eventLoopGroup,
localAddress,
ImmutableList.of(new RedirectHandler(redirectInput, destinationInput, destinationPath)));
}
public static TestServer eppServer(EventLoopGroup eventLoopGroup, LocalAddress localAddress) {
// TODO - add LengthFieldBasedFrameDecoder to handlers in pipeline if necessary for future
// tests.
return new TestServer(eventLoopGroup, localAddress, ImmutableList.of(new EppHandler()));
}
/** Handler that will wither redirect client, give successful response, or give error messge */
@Sharable
static class RedirectHandler extends SimpleChannelInboundHandler<HttpRequest> {
private String redirectInput;
private String destinationInput;
private String destinationPath;
/**
* @param redirectInput - Server will send back redirect to {@code destinationInput} when
* receiving a request with this host location
* @param destinationInput - Server will send back an {@link HttpResponseStatus} OK response
* when receiving a request with this host location
*/
RedirectHandler(String redirectInput, String destinationInput, String destinationPath) {
this.redirectInput = redirectInput;
this.destinationInput = destinationInput;
this.destinationPath = destinationPath;
}
/**
* Reads input {@link HttpRequest}, and creates appropriate {@link HttpResponseMessage} based on
* what header location is
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, HttpRequest request) {
HttpResponse response;
if (request.headers().get("host").equals(redirectInput)) {
response =
new HttpResponseMessage(
makeRedirectResponse(HttpResponseStatus.MOVED_PERMANENTLY, destinationInput, true));
} else if (request.headers().get("host").equals(destinationInput)
&& request.uri().equals(destinationPath)) {
response = new HttpResponseMessage(makeHttpResponse(HttpResponseStatus.OK));
} else {
response = new HttpResponseMessage(makeHttpResponse(HttpResponseStatus.BAD_REQUEST));
}
ChannelFuture unusedFuture = ctx.channel().writeAndFlush(response);
}
}
private static class EppHandler extends ChannelDuplexHandler {
private ChannelPromise future;
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
// TODO - pass EppMessage into future to easily read attributes of message.
future = ctx.newPromise();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
byte[] messageBytes = new byte[buf.readableBytes()];
buf.readBytes(messageBytes);
// TODO - Break ByteBuf into multiple pieces to test how well it works with
// LengthFieldBasedFrameDecoder.
try {
EppMessage.byteArrayToXmlDoc(messageBytes);
ChannelFuture unusedFuture = future.setSuccess();
} catch (FailureException e) {
ChannelFuture unusedFuture = future.setFailure(e);
}
}
}
}

View File

@@ -23,7 +23,7 @@ import io.netty.channel.Channel;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
/** Unit Tests for each {@link Token} subtype (just {@link WebWhoisToken} for now) */
/** Unit Tests for each {@link Token} subtype (just {@link EppToken} for now) */
class EppTokenTest {
private static String TEST_HOST = "host";

View File

@@ -1,67 +0,0 @@
// Copyright 2019 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.monitoring.blackbox.token;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableList;
import google.registry.monitoring.blackbox.exception.UndeterminedStateException;
import google.registry.monitoring.blackbox.message.HttpRequestMessage;
import org.junit.jupiter.api.Test;
/** Unit Tests for {@link WebWhoisToken} */
class WebWhoisTokenTest {
private static final String PREFIX = "whois.nic.";
private static final String HOST = "starter";
private static final String FIRST_TLD = "first_test";
private static final String SECOND_TLD = "second_test";
private static final String THIRD_TLD = "third_test";
private final ImmutableList<String> testDomains =
ImmutableList.of(FIRST_TLD, SECOND_TLD, THIRD_TLD);
private Token webToken = new WebWhoisToken(testDomains);
@Test
void testMessageModification() throws UndeterminedStateException {
// creates Request message with header
HttpRequestMessage message = new HttpRequestMessage();
message.headers().set("host", HOST);
// attempts to use Token's method for modifying the method based on its stored host
HttpRequestMessage secondMessage = (HttpRequestMessage) webToken.modifyMessage(message);
assertThat(secondMessage.headers().get("host")).isEqualTo(PREFIX + FIRST_TLD);
}
@Test
void testHostOfToken() {
assertThat(webToken.host()).isEqualTo(PREFIX + FIRST_TLD);
assertThat(webToken.host()).isEqualTo(PREFIX + FIRST_TLD);
}
@Test
void testNextToken() {
assertThat(webToken.host()).isEqualTo(PREFIX + FIRST_TLD);
webToken = webToken.next();
assertThat(webToken.host()).isEqualTo(PREFIX + SECOND_TLD);
webToken = webToken.next();
assertThat(webToken.host()).isEqualTo(PREFIX + THIRD_TLD);
webToken = webToken.next();
assertThat(webToken.host()).isEqualTo(PREFIX + FIRST_TLD);
}
}

View File

@@ -1,65 +0,0 @@
// Copyright 2019 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.monitoring.blackbox.util;
import static java.nio.charset.StandardCharsets.US_ASCII;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
/** Houses static utility functions for testing WebWHOIS components of Prober. */
public class WebWhoisUtils {
public static FullHttpRequest makeHttpGetRequest(String host, String path) {
FullHttpRequest request =
new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path);
request.headers().set("host", host).setInt("content-length", 0);
return request;
}
public static FullHttpResponse makeHttpResponse(String content, HttpResponseStatus status) {
ByteBuf buf = Unpooled.wrappedBuffer(content.getBytes(US_ASCII));
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, buf);
response.headers().setInt("content-length", buf.readableBytes());
return response;
}
public static FullHttpResponse makeHttpResponse(HttpResponseStatus status) {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status);
response.headers().setInt("content-length", 0);
return response;
}
/** Creates HttpResponse given status, redirection location, and other necessary inputs */
public static FullHttpResponse makeRedirectResponse(
HttpResponseStatus status, String location, boolean keepAlive) {
FullHttpResponse response = makeHttpResponse("", status);
response.headers().set("content-type", "text/plain");
if (location != null) {
response.headers().set("location", location);
}
if (keepAlive) {
response.headers().set("connection", "keep-alive");
}
return response;
}
}

View File

@@ -39,7 +39,7 @@ import javax.annotation.Nullable;
*
* <p>Only a builder is provided because the client protocol itself depends on the remote host
* address, which is provided in the server protocol module that relays to this client protocol
* module, e.g., {@link WhoisProtocolModule}.
* module, e.g., {@link EppProtocolModule}.
*
* <p>The protocol can be configured without TLS. In this case, the remote host has to be
* "localhost". Plan HTTP is only expected to be used when communication with Nomulus is via local

View File

@@ -47,9 +47,7 @@ public class ProxyConfig {
public Gcs gcs;
public Kms kms;
public Epp epp;
public Whois whois;
public HealthCheck healthCheck;
public WebWhois webWhois;
public HttpsRelay httpsRelay;
public Metrics metrics;
@@ -77,16 +75,6 @@ public class ProxyConfig {
public Quota quota;
}
/** Configuration options that apply to WHOIS protocol. */
public static class Whois {
public int port;
public String relayHost;
public String relayPath;
public int maxMessageLengthBytes;
public int readTimeoutSeconds;
public Quota quota;
}
/** Configuration options that apply to GCP load balancer health check protocol. */
public static class HealthCheck {
public int port;
@@ -94,13 +82,6 @@ public class ProxyConfig {
public String checkResponse;
}
/** Configuration options that apply to web WHOIS redirects. */
public static class WebWhois {
public int httpPort;
public int httpsPort;
public String redirectHost;
}
/** Configuration options that apply to HTTPS relay protocol. */
public static class HttpsRelay {
public int port;

View File

@@ -40,9 +40,6 @@ import google.registry.proxy.HealthCheckProtocolModule.HealthCheckProtocol;
import google.registry.proxy.HttpsRelayProtocolModule.HttpsRelayProtocol;
import google.registry.proxy.Protocol.FrontendProtocol;
import google.registry.proxy.ProxyConfig.Environment;
import google.registry.proxy.WebWhoisProtocolsModule.HttpWhoisProtocol;
import google.registry.proxy.WebWhoisProtocolsModule.HttpsWhoisProtocol;
import google.registry.proxy.WhoisProtocolModule.WhoisProtocol;
import google.registry.proxy.handler.ProxyProtocolHandler;
import google.registry.util.Clock;
import google.registry.util.GcpJsonFormatter;
@@ -79,25 +76,16 @@ import java.util.logging.Level;
@Module
public class ProxyModule {
@Parameter(names = "--whois", description = "Port for WHOIS")
private Integer whoisPort;
@Parameter(names = "--epp", description = "Port for EPP")
private Integer eppPort;
@Parameter(names = "--health_check", description = "Port for health check")
private Integer healthCheckPort;
@Parameter(names = "--http_whois", description = "Port for HTTP WHOIS")
private Integer httpWhoisPort;
@Parameter(names = "--https_whois", description = "Port for HTTPS WHOIS")
private Integer httpsWhoisPort;
@Parameter(
names = "--local",
description =
"Whether EPP/WHOIS traffic should be forwarded to localhost using HTTP on port defined in"
"Whether EPP traffic should be forwarded to localhost using HTTP on port defined in"
+ " httpsRelay.localPort")
private boolean local = false;
@@ -184,12 +172,6 @@ public class ProxyModule {
return local;
}
@Provides
@WhoisProtocol
int provideWhoisPort(ProxyConfig config) {
return Optional.ofNullable(whoisPort).orElse(config.whois.port);
}
@Provides
@EppProtocol
int provideEppPort(ProxyConfig config) {
@@ -202,18 +184,6 @@ public class ProxyModule {
return Optional.ofNullable(healthCheckPort).orElse(config.healthCheck.port);
}
@Provides
@HttpWhoisProtocol
int provideHttpWhoisProtocol(ProxyConfig config) {
return Optional.ofNullable(httpWhoisPort).orElse(config.webWhois.httpPort);
}
@Provides
@HttpsWhoisProtocol
int provideHttpsWhoisProtocol(ProxyConfig config) {
return Optional.ofNullable(httpsWhoisPort).orElse(config.webWhois.httpsPort);
}
@Provides
Environment provideEnvironment() {
return env;
@@ -423,8 +393,6 @@ public class ProxyModule {
ProxyModule.class,
CertificateSupplierModule.class,
HttpsRelayProtocolModule.class,
WhoisProtocolModule.class,
WebWhoisProtocolsModule.class,
EppProtocolModule.class,
HealthCheckProtocolModule.class,
MetricsModule.class

View File

@@ -93,8 +93,7 @@ public class ProxyServer implements Runnable {
addHandlers(inboundChannel.pipeline(), inboundProtocol.handlerProviders());
if (!inboundProtocol.hasBackend()) {
// If the frontend has no backend to relay to (health check, web WHOIS redirect, etc), start
// reading immediately.
// If the frontend has no backend to relay to (e.g. health check) start reading immediately.
inboundChannel.config().setAutoRead(true);
} else {
logger.atInfo().log(

View File

@@ -1,140 +0,0 @@
// Copyright 2018 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.proxy;
import com.google.common.collect.ImmutableList;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
import google.registry.networking.handler.SslServerInitializer;
import google.registry.proxy.Protocol.FrontendProtocol;
import google.registry.proxy.handler.WebWhoisRedirectHandler;
import io.netty.channel.ChannelHandler;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerExpectContinueHandler;
import io.netty.handler.ssl.SslProvider;
import jakarta.inject.Provider;
import jakarta.inject.Qualifier;
import jakarta.inject.Singleton;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.function.Supplier;
/** A module that provides the {@link FrontendProtocol}s to redirect HTTP(S) web WHOIS requests. */
@Module
public final class WebWhoisProtocolsModule {
private WebWhoisProtocolsModule() {}
/** Dagger qualifier to provide HTTP whois protocol related handlers and other bindings. */
@Qualifier
@interface HttpWhoisProtocol {}
/** Dagger qualifier to provide HTTPS whois protocol related handlers and other bindings. */
@Qualifier
@interface HttpsWhoisProtocol {}
private static final String HTTP_PROTOCOL_NAME = "whois_http";
private static final String HTTPS_PROTOCOL_NAME = "whois_https";
@Singleton
@Provides
@IntoSet
static FrontendProtocol provideHttpWhoisProtocol(
@HttpWhoisProtocol int httpWhoisPort,
@HttpWhoisProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
return Protocol.frontendBuilder()
.name(HTTP_PROTOCOL_NAME)
.port(httpWhoisPort)
.hasBackend(false)
.handlerProviders(handlerProviders)
.build();
}
@Singleton
@Provides
@IntoSet
static FrontendProtocol provideHttpsWhoisProtocol(
@HttpsWhoisProtocol int httpsWhoisPort,
@HttpsWhoisProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
return Protocol.frontendBuilder()
.name(HTTPS_PROTOCOL_NAME)
.port(httpsWhoisPort)
.hasBackend(false)
.handlerProviders(handlerProviders)
.build();
}
@Provides
@HttpWhoisProtocol
static ImmutableList<Provider<? extends ChannelHandler>> providerHttpWhoisHandlerProviders(
Provider<HttpServerCodec> httpServerCodecProvider,
Provider<HttpServerExpectContinueHandler> httpServerExpectContinueHandlerProvider,
@HttpWhoisProtocol Provider<WebWhoisRedirectHandler> webWhoisRedirectHandlerProvides) {
return ImmutableList.of(
httpServerCodecProvider,
httpServerExpectContinueHandlerProvider,
webWhoisRedirectHandlerProvides);
}
@Provides
@HttpsWhoisProtocol
static ImmutableList<Provider<? extends ChannelHandler>> providerHttpsWhoisHandlerProviders(
@HttpsWhoisProtocol
Provider<SslServerInitializer<NioSocketChannel>> sslServerInitializerProvider,
Provider<HttpServerCodec> httpServerCodecProvider,
Provider<HttpServerExpectContinueHandler> httpServerExpectContinueHandlerProvider,
@HttpsWhoisProtocol Provider<WebWhoisRedirectHandler> webWhoisRedirectHandlerProvides) {
return ImmutableList.of(
sslServerInitializerProvider,
httpServerCodecProvider,
httpServerExpectContinueHandlerProvider,
webWhoisRedirectHandlerProvides);
}
@Provides
static HttpServerCodec provideHttpServerCodec() {
return new HttpServerCodec();
}
@Provides
@HttpWhoisProtocol
static WebWhoisRedirectHandler provideHttpRedirectHandler(ProxyConfig config) {
return new WebWhoisRedirectHandler(false, config.webWhois.redirectHost);
}
@Provides
@HttpsWhoisProtocol
static WebWhoisRedirectHandler provideHttpsRedirectHandler(ProxyConfig config) {
return new WebWhoisRedirectHandler(true, config.webWhois.redirectHost);
}
@Provides
static HttpServerExpectContinueHandler provideHttpServerExpectContinueHandler() {
return new HttpServerExpectContinueHandler();
}
@Singleton
@Provides
@HttpsWhoisProtocol
static SslServerInitializer<NioSocketChannel> provideSslServerInitializer(
SslProvider sslProvider,
Supplier<PrivateKey> privateKeySupplier,
Supplier<ImmutableList<X509Certificate>> certificatesSupplier) {
return new SslServerInitializer<>(
false, false, sslProvider, privateKeySupplier, certificatesSupplier);
}
}

View File

@@ -1,136 +0,0 @@
// 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.proxy;
import com.google.common.collect.ImmutableList;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
import google.registry.proxy.HttpsRelayProtocolModule.HttpsRelayProtocol;
import google.registry.proxy.Protocol.BackendProtocol;
import google.registry.proxy.Protocol.FrontendProtocol;
import google.registry.proxy.handler.FrontendMetricsHandler;
import google.registry.proxy.handler.ProxyProtocolHandler;
import google.registry.proxy.handler.QuotaHandler.WhoisQuotaHandler;
import google.registry.proxy.handler.RelayHandler.FullHttpRequestRelayHandler;
import google.registry.proxy.handler.WhoisServiceHandler;
import google.registry.proxy.metric.FrontendMetrics;
import google.registry.proxy.quota.QuotaConfig;
import google.registry.proxy.quota.QuotaManager;
import google.registry.proxy.quota.TokenStore;
import google.registry.util.Clock;
import io.netty.channel.ChannelHandler;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.timeout.ReadTimeoutHandler;
import jakarta.inject.Named;
import jakarta.inject.Provider;
import jakarta.inject.Qualifier;
import jakarta.inject.Singleton;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Supplier;
/** A module that provides the {@link FrontendProtocol} used for whois protocol. */
@Module
public final class WhoisProtocolModule {
private WhoisProtocolModule() {}
/** Dagger qualifier to provide whois protocol related handlers and other bindings. */
@Qualifier
public @interface WhoisProtocol {}
private static final String PROTOCOL_NAME = "whois";
@Singleton
@Provides
@IntoSet
static FrontendProtocol provideProtocol(
ProxyConfig config,
@WhoisProtocol int whoisPort,
@WhoisProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders,
@HttpsRelayProtocol BackendProtocol.Builder backendProtocolBuilder,
@HttpsRelayProtocol boolean localRelay) {
return Protocol.frontendBuilder()
.name(PROTOCOL_NAME)
.port(whoisPort)
.handlerProviders(handlerProviders)
.relayProtocol(
backendProtocolBuilder.host(localRelay ? "localhost" : config.whois.relayHost).build())
.build();
}
@Provides
@WhoisProtocol
static ImmutableList<Provider<? extends ChannelHandler>> provideHandlerProviders(
Provider<ProxyProtocolHandler> proxyProtocolHandlerProvider,
@WhoisProtocol Provider<ReadTimeoutHandler> readTimeoutHandlerProvider,
Provider<LineBasedFrameDecoder> lineBasedFrameDecoderProvider,
Provider<WhoisServiceHandler> whoisServiceHandlerProvider,
Provider<FrontendMetricsHandler> frontendMetricsHandlerProvider,
Provider<WhoisQuotaHandler> whoisQuotaHandlerProvider,
Provider<FullHttpRequestRelayHandler> relayHandlerProvider) {
return ImmutableList.of(
proxyProtocolHandlerProvider,
readTimeoutHandlerProvider,
lineBasedFrameDecoderProvider,
whoisServiceHandlerProvider,
frontendMetricsHandlerProvider,
whoisQuotaHandlerProvider,
relayHandlerProvider);
}
@Provides
static WhoisServiceHandler provideWhoisServiceHandler(
ProxyConfig config,
@Named("idToken") Supplier<String> idTokenSupplier,
@Named("canary") boolean canary,
FrontendMetrics metrics,
@HttpsRelayProtocol boolean localRelay) {
return new WhoisServiceHandler(
localRelay ? "localhost" : config.whois.relayHost,
config.whois.relayPath,
canary,
idTokenSupplier,
metrics);
}
@Provides
static LineBasedFrameDecoder provideLineBasedFrameDecoder(ProxyConfig config) {
return new LineBasedFrameDecoder(config.whois.maxMessageLengthBytes);
}
@Provides
@WhoisProtocol
static ReadTimeoutHandler provideReadTimeoutHandler(ProxyConfig config) {
return new ReadTimeoutHandler(config.whois.readTimeoutSeconds);
}
@Provides
@WhoisProtocol
static TokenStore provideTokenStore(
ProxyConfig config, ScheduledExecutorService refreshExecutor, Clock clock) {
return new TokenStore(
new QuotaConfig(config.whois.quota, PROTOCOL_NAME), refreshExecutor, clock);
}
@Provides
@Singleton
@WhoisProtocol
static QuotaManager provideQuotaManager(
@WhoisProtocol TokenStore tokenStore, ExecutorService executorService) {
return new QuotaManager(tokenStore, executorService);
}
}

View File

@@ -118,54 +118,6 @@ epp:
# defaultQuota for list entries.
customQuota: []
whois:
port: 30001
relayHost: registry-project-id.appspot.com
relayPath: /_dr/whois
# Maximum input message length in bytes.
#
# Domain name cannot be longer than 256 characters. 512-character message
# length should be safe for most cases, including registrar queries.
#
# See also: RFC 1035 2.3.4 Size limits
# (http://www.freesoft.org/CIE/RFC/1035/9.htm).
maxMessageLengthBytes: 512
# Whois protocol is transient, the client should not establish a long-lasting
# idle connection.
readTimeoutSeconds: 60
# Quota configuration for WHOIS
quota:
# Token database refresh period. Set to 0 to disable refresh.
#
# After the set time period, inactive token buckets will be deleted.
refreshSeconds: 3600
# Default quota for any userId not matched in customQuota.
defaultQuota:
# List of identifiers, e.g. IP address, certificate hash.
#
# userId for defaultQuota should always be an empty list.
userId: []
# Number of tokens allotted to the matched user. Set to -1 to allow
# infinite quota.
tokenAmount: 100
# Token refill period. Set to 0 to disable refill.
#
# After the set time period, the token for the given user will be
# reset to tokenAmount.
refillSeconds: 600
# List of custom quotas for specific userId. Use the same schema as
# defaultQuota for list entries.
customQuota: []
healthCheck:
port: 30000
@@ -181,14 +133,6 @@ httpsRelay:
# Maximum size of an HTTP message in bytes.
maxMessageLengthBytes: 524288
webWhois:
httpPort: 30010
httpsPort: 30011
# The 302 redirect destination of HTTPS web WHOIS GET requests.
# HTTP web WHOIS GET requests will be 301 redirected to HTTPS first.
redirectHost: whois.yourdomain.tld
metrics:
# Max queries per second for the Google Cloud Monitoring V3 (aka Stackdriver)
# API. The limit can be adjusted by contacting Cloud Support.

View File

@@ -104,7 +104,6 @@ public class BackendMetricsHandler extends ChannelDuplexHandler {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
checkArgument(msg instanceof FullHttpRequest, "Outgoing request must be FullHttpRequest.");
// For WHOIS, client certificate hash is always set to "none".
// For EPP, the client hash attribute is set upon handshake completion, before the first HELLO
// is sent to the server. Therefore the first call to write() with HELLO payload has access to
// the hash in its channel attribute.

View File

@@ -52,18 +52,14 @@ public class FrontendMetricsHandler extends ChannelDuplexHandler {
*
* <p>This queue is used to calculate frontend request-response latency.
*
* <p>For the WHOIS protocol, the TCP connection closes after one request-response round trip and
* the request always comes first. The queue for WHOIS therefore only need to store one value.
*
* <p>For the EPP protocol, the specification allows for pipelining, in which a client can sent
* multiple requests without waiting for each responses. Therefore a queue is needed to record all
* multiple requests without waiting for each response. Therefore, a queue is needed to record all
* the requests that are sent but have not yet received a response.
*
* <p>A server must send its response in the same order it receives requests. This invariance
* guarantees that the request time at the head of the queue always corresponds to the response
* received in {@link #channelRead}.
*
* @see <a href="https://tools.ietf.org/html/rfc3912">RFC 3912 WHOIS Protocol Specification</a>
* @see <a href="https://tools.ietf.org/html/rfc5734#section-3">RFC 5734 Extensible Provisioning
* Protocol (EPP) Transport over TCP</a>
*/
@@ -97,7 +93,6 @@ public class FrontendMetricsHandler extends ChannelDuplexHandler {
// increase in size if more requests are received from the client, but that does not invalidate
// this check.
checkState(!requestReceivedTimeQueue.isEmpty(), "Response sent before request is received.");
// For WHOIS, client certificate hash is always set to "none".
// For EPP, the client hash attribute is set upon handshake completion, before the first HELLO
// is sent to the server. Therefore the first call to write() with HELLO payload has access to
// the hash in its channel attribute.

View File

@@ -17,10 +17,8 @@ package google.registry.proxy.handler;
import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.proxy.Protocol.PROTOCOL_KEY;
import static google.registry.proxy.handler.EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY;
import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
import google.registry.proxy.EppProtocolModule.EppProtocol;
import google.registry.proxy.WhoisProtocolModule.WhoisProtocol;
import google.registry.proxy.metric.FrontendMetrics;
import google.registry.proxy.quota.QuotaManager;
import google.registry.proxy.quota.QuotaManager.QuotaRebate;
@@ -83,42 +81,6 @@ public abstract class QuotaHandler extends ChannelInboundHandlerAdapter {
}
}
/** Quota Handler for WHOIS protocol. */
public static class WhoisQuotaHandler extends QuotaHandler {
@Inject
WhoisQuotaHandler(@WhoisProtocol QuotaManager quotaManager, FrontendMetrics metrics) {
super(quotaManager, metrics);
}
/**
* Reads user ID from channel attribute {@code REMOTE_ADDRESS_KEY}.
*
* <p>This attribute is set by {@link ProxyProtocolHandler} when the first frame of message is
* read.
*/
@Override
String getUserId(ChannelHandlerContext ctx) {
return ctx.channel().attr(REMOTE_ADDRESS_KEY).get();
}
@Override
boolean isUserIdPii() {
return true;
}
/**
* Do nothing when connection terminates.
*
* <p>WHOIS protocol is configured with a QPS type quota, there is no need to return the tokens
* back to the quota store because the quota store will auto-refill tokens based on the QPS.
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) {
ctx.fireChannelInactive();
}
}
/** Quota Handler for EPP protocol. */
public static class EppQuotaHandler extends QuotaHandler {

View File

@@ -1,145 +0,0 @@
// Copyright 2018 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.proxy.handler;
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaderNames.HOST;
import static io.netty.handler.codec.http.HttpHeaderNames.LOCATION;
import static io.netty.handler.codec.http.HttpHeaderValues.KEEP_ALIVE;
import static io.netty.handler.codec.http.HttpHeaderValues.TEXT_PLAIN;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpMethod.HEAD;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED;
import static io.netty.handler.codec.http.HttpResponseStatus.MOVED_PERMANENTLY;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpUtil;
import java.time.Duration;
/**
* Handler that redirects web WHOIS requests to a canonical website.
*
* <p>ICANN requires that port 43 and web-based WHOIS are both available on whois.nic.TLD. Since we
* expose a single IPv4/IPv6 anycast external IP address for the proxy, we need the load balancer to
* router port 80/443 traffic to the proxy to support web WHOIS.
*
* <p>HTTP (port 80) traffic is simply upgraded to HTTPS (port 443) on the same host, while HTTPS
* requests are redirected to the {@code redirectHost}, which is the canonical website that provide
* the web WHOIS service.
*
* @see <a
* href="https://newgtlds.icann.org/sites/default/files/agreements/agreement-approved-31jul17-en.html">
* REGISTRY AGREEMENT</a>
*/
public class WebWhoisRedirectHandler extends SimpleChannelInboundHandler<HttpRequest> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
* HTTP health check sent by GCP HTTP load balancer is set to use this host name.
*
* <p>Status 200 must be returned in order for a health check to be considered successful.
*
* @see <a
* href="https://cloud.google.com/load-balancing/docs/health-check-concepts#http_https_and_http2_health_checks">
* HTTP, HTTPS, and HTTP/2 health checks</a>
*/
private static final String HEALTH_CHECK_HOST = "health-check.invalid";
private static final String HSTS_HEADER_NAME = "Strict-Transport-Security";
private static final Duration HSTS_MAX_AGE = Duration.ofDays(365);
private static final ImmutableList<HttpMethod> ALLOWED_METHODS = ImmutableList.of(GET, HEAD);
private final boolean isHttps;
private final String redirectHost;
public WebWhoisRedirectHandler(boolean isHttps, String redirectHost) {
this.isHttps = isHttps;
this.redirectHost = redirectHost;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) {
FullHttpResponse response;
if (!ALLOWED_METHODS.contains(msg.method())) {
response = new DefaultFullHttpResponse(HTTP_1_1, METHOD_NOT_ALLOWED);
} else if (Strings.isNullOrEmpty(msg.headers().get(HOST))) {
response = new DefaultFullHttpResponse(HTTP_1_1, BAD_REQUEST);
} else {
// All HTTP/1.1 request must contain a Host header with the format "host:[port]".
// See https://tools.ietf.org/html/rfc2616#section-14.23
String host = Splitter.on(':').split(msg.headers().get(HOST)).iterator().next();
if (host.equals(HEALTH_CHECK_HOST)) {
// The health check request should always be sent to the HTTP port.
response =
isHttps
? new DefaultFullHttpResponse(HTTP_1_1, FORBIDDEN)
: new DefaultFullHttpResponse(HTTP_1_1, OK);
} else {
// HTTP -> HTTPS is a 301 redirect, whereas HTTPS -> web WHOIS site is 302 redirect.
response = new DefaultFullHttpResponse(HTTP_1_1, isHttps ? FOUND : MOVED_PERMANENTLY);
String redirectUrl = String.format("https://%s/", isHttps ? redirectHost : host);
response.headers().set(LOCATION, redirectUrl);
// Add HSTS header to HTTPS response.
if (isHttps) {
response
.headers()
.set(HSTS_HEADER_NAME, String.format("max-age=%d", HSTS_MAX_AGE.getSeconds()));
}
}
}
// Common headers that need to be set on any response.
response
.headers()
.set(CONTENT_TYPE, TEXT_PLAIN)
.setInt(CONTENT_LENGTH, response.content().readableBytes());
// Close the connection if keep-alive is not set in the request.
if (!HttpUtil.isKeepAlive(msg)) {
ChannelFuture unusedFuture =
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
} else {
response.headers().set(CONNECTION, KEEP_ALIVE);
ChannelFuture unusedFuture = ctx.writeAndFlush(response);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.atWarning().withCause(cause).log(
(isHttps ? "HTTPS" : "HTTP") + " WHOIS inbound exception caught for channel %s",
ctx.channel());
ChannelFuture unusedFuture = ctx.close();
}
}

View File

@@ -1,83 +0,0 @@
// 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.proxy.handler;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
import google.registry.proxy.metric.FrontendMetrics;
import google.registry.util.ProxyHttpHeaders;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpResponse;
import java.util.function.Supplier;
/** Handler that processes WHOIS protocol logic. */
public final class WhoisServiceHandler extends HttpsRelayServiceHandler {
private String clientAddress;
public WhoisServiceHandler(
String relayHost,
String relayPath,
boolean canary,
Supplier<String> idTokenSupplier,
FrontendMetrics metrics) {
super(relayHost, relayPath, canary, idTokenSupplier, metrics);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
metrics.registerActiveConnection("whois", "none", ctx.channel());
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
clientAddress = ctx.channel().attr(REMOTE_ADDRESS_KEY).get();
super.channelRead(ctx, msg);
}
@Override
protected FullHttpRequest decodeFullHttpRequest(ByteBuf byteBuf) {
FullHttpRequest request = super.decodeFullHttpRequest(byteBuf);
request
.headers()
.set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN)
.set(HttpHeaderNames.ACCEPT, HttpHeaderValues.TEXT_PLAIN);
if (clientAddress != null) {
request
.headers()
.set(ProxyHttpHeaders.IP_ADDRESS, clientAddress)
.set(ProxyHttpHeaders.FALLBACK_IP_ADDRESS, clientAddress);
}
return request;
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
throws Exception {
// Close connection after a response is received, per RFC-3912
// https://tools.ietf.org/html/rfc3912
checkArgument(msg instanceof HttpResponse);
promise.addListener(ChannelFutureListener.CLOSE);
super.write(ctx, msg, promise);
}
}

View File

@@ -27,9 +27,8 @@ public abstract class BaseMetrics {
/**
* Labels to register metrics with.
*
* <p>The client certificate hash value is only used for EPP metrics. For WHOIS metrics, it will
* always be {@code "none"}. In order to get the actual registrar name, one can use the {@code
* nomulus} tool:
* <p>The client certificate hash value is only used for EPP metrics. In order to get the actual
* registrar name, one can use the {@code nomulus} tool:
*
* <pre>
* nomulus -e production list_registrars -f clientCertificateHash | grep $HASH

View File

@@ -33,16 +33,12 @@ import google.registry.proxy.EppProtocolModule.EppProtocol;
import google.registry.proxy.HealthCheckProtocolModule.HealthCheckProtocol;
import google.registry.proxy.HttpsRelayProtocolModule.HttpsRelayProtocol;
import google.registry.proxy.ProxyConfig.Environment;
import google.registry.proxy.WebWhoisProtocolsModule.HttpWhoisProtocol;
import google.registry.proxy.WhoisProtocolModule.WhoisProtocol;
import google.registry.proxy.handler.BackendMetricsHandler;
import google.registry.proxy.handler.FrontendMetricsHandler;
import google.registry.proxy.handler.ProxyProtocolHandler;
import google.registry.proxy.handler.QuotaHandler.EppQuotaHandler;
import google.registry.proxy.handler.QuotaHandler.WhoisQuotaHandler;
import google.registry.proxy.handler.RelayHandler.FullHttpRequestRelayHandler;
import google.registry.proxy.handler.RelayHandler.FullHttpResponseRelayHandler;
import google.registry.proxy.handler.WebWhoisRedirectHandler;
import google.registry.testing.FakeClock;
import google.registry.util.Clock;
import io.netty.channel.Channel;
@@ -102,18 +98,12 @@ public abstract class ProtocolModuleTest {
// tested separately in their respective unit tests.
FullHttpRequestRelayHandler.class,
FullHttpResponseRelayHandler.class,
// This handler is tested in its own unit tests. It is installed in web whois redirect
// protocols. The end-to-end tests for the rest of the handlers in its pipeline need to
// be able to emit incoming requests out of the channel for assertions. Therefore, this
// handler is removed from the pipeline.
WebWhoisRedirectHandler.class,
// The rest are not part of business logic and do not need to be tested, obviously.
LoggingHandler.class,
// Metrics instrumentation is tested separately.
BackendMetricsHandler.class,
FrontendMetricsHandler.class,
// Quota management is tested separately.
WhoisQuotaHandler.class,
EppQuotaHandler.class,
ReadTimeoutHandler.class);
@@ -190,17 +180,12 @@ public abstract class ProtocolModuleTest {
modules = {
TestModule.class,
CertificateSupplierModule.class,
WhoisProtocolModule.class,
WebWhoisProtocolsModule.class,
EppProtocolModule.class,
HealthCheckProtocolModule.class,
HttpsRelayProtocolModule.class
})
interface TestComponent {
@WhoisProtocol
ImmutableList<Provider<? extends ChannelHandler>> whoisHandlers();
@EppProtocol
ImmutableList<Provider<? extends ChannelHandler>> eppHandlers();
@@ -209,9 +194,6 @@ public abstract class ProtocolModuleTest {
@HttpsRelayProtocol
ImmutableList<Provider<? extends ChannelHandler>> httpsRelayHandlers();
@HttpWhoisProtocol
ImmutableList<Provider<? extends ChannelHandler>> httpWhoisHandlers();
}
/**

View File

@@ -33,14 +33,9 @@ class ProxyModuleTest {
void testSuccess_parseArgs_defaultArgs() {
String[] args = {};
proxyModule.parse(args);
assertThat(proxyModule.provideWhoisPort(PROXY_CONFIG)).isEqualTo(PROXY_CONFIG.whois.port);
assertThat(proxyModule.provideEppPort(PROXY_CONFIG)).isEqualTo(PROXY_CONFIG.epp.port);
assertThat(proxyModule.provideHealthCheckPort(PROXY_CONFIG))
.isEqualTo(PROXY_CONFIG.healthCheck.port);
assertThat(proxyModule.provideHttpWhoisProtocol(PROXY_CONFIG))
.isEqualTo(PROXY_CONFIG.webWhois.httpPort);
assertThat(proxyModule.provideHttpsWhoisProtocol(PROXY_CONFIG))
.isEqualTo(PROXY_CONFIG.webWhois.httpsPort);
assertThat(proxyModule.provideEnvironment()).isEqualTo(LOCAL);
assertThat(proxyModule.log).isFalse();
}
@@ -74,13 +69,6 @@ class ProxyModuleTest {
assertThat(proxyModule.log).isTrue();
}
@Test
void testSuccess_parseArgs_customWhoisPort() {
String[] args = {"--whois", "12345"};
proxyModule.parse(args);
assertThat(proxyModule.provideWhoisPort(PROXY_CONFIG)).isEqualTo(12345);
}
@Test
void testSuccess_parseArgs_customEppPort() {
String[] args = {"--epp", "22222"};
@@ -95,20 +83,6 @@ class ProxyModuleTest {
assertThat(proxyModule.provideHealthCheckPort(PROXY_CONFIG)).isEqualTo(23456);
}
@Test
void testSuccess_parseArgs_customhttpWhoisPort() {
String[] args = {"--http_whois", "12121"};
proxyModule.parse(args);
assertThat(proxyModule.provideHttpWhoisProtocol(PROXY_CONFIG)).isEqualTo(12121);
}
@Test
void testSuccess_parseArgs_customhttpsWhoisPort() {
String[] args = {"--https_whois", "21212"};
proxyModule.parse(args);
assertThat(proxyModule.provideHttpsWhoisProtocol(PROXY_CONFIG)).isEqualTo(21212);
}
@Test
void testSuccess_parseArgs_customEnvironment() {
String[] args = {"--env", "ALpHa"};

View File

@@ -88,22 +88,6 @@ public final class TestUtils {
return response;
}
public static FullHttpRequest makeWhoisHttpRequest(
String content, String host, String path, boolean canary, String idToken) throws IOException {
FullHttpRequest request = makeHttpPostRequest(content, host, path, canary);
request
.headers()
.set("authorization", "Bearer " + idToken)
.set("content-type", "text/plain")
.set("accept", "text/plain");
return request;
}
public static FullHttpRequest makeWhoisHttpRequest(
String content, String host, String path, String idToken) throws IOException {
return makeWhoisHttpRequest(content, host, path, false, idToken);
}
public static FullHttpRequest makeEppHttpRequest(
String content,
String host,
@@ -142,12 +126,6 @@ public final class TestUtils {
content, host, path, false, idToken, sslClientCertificateHash, clientAddress, cookies);
}
public static FullHttpResponse makeWhoisHttpResponse(String content, HttpResponseStatus status) {
FullHttpResponse response = makeHttpResponse(content, status);
response.headers().set("content-type", "text/plain");
return response;
}
public static FullHttpResponse makeEppHttpResponse(
String content, HttpResponseStatus status, Cookie... cookies) {
FullHttpResponse response = makeHttpResponse(content, status);

Some files were not shown because too many files have changed in this diff Show More