Import code from internal repository to git

This commit is contained in:
Justine Tunney
2016-03-01 17:59:16 -05:00
commit 0ef0c933d2
2490 changed files with 281594 additions and 0 deletions
@@ -0,0 +1,30 @@
package(
default_visibility = ["//java/com/google/domain/registry:registry_project"],
)
java_library(
name = "whois",
srcs = glob(["*.java"]),
resources = ["disclaimer.txt"],
deps = [
"//java/com/google/common/annotations",
"//java/com/google/common/base",
"//java/com/google/common/collect",
"//java/com/google/common/html",
"//java/com/google/common/io",
"//java/com/google/common/net",
"//java/com/google/domain/registry/config",
"//java/com/google/domain/registry/model",
"//java/com/google/domain/registry/request",
"//java/com/google/domain/registry/util",
"//java/com/google/domain/registry/xml",
"//third_party/java/appengine:appengine-api",
"//third_party/java/dagger",
"//third_party/java/joda_time",
"//third_party/java/jsr305_annotations",
"//third_party/java/jsr330_inject",
"//third_party/java/objectify:objectify-v4_1",
"//third_party/java/servlet/servlet_api",
],
)
@@ -0,0 +1,39 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import com.google.common.net.InternetDomainName;
import com.google.domain.registry.model.domain.DomainResource;
import org.joda.time.DateTime;
import javax.annotation.Nullable;
/** Represents a WHOIS lookup on a domain name (i.e. SLD). */
class DomainLookupCommand extends DomainOrHostLookupCommand<DomainResource> {
DomainLookupCommand(InternetDomainName domainName) {
this(domainName, null);
}
public DomainLookupCommand(InternetDomainName domainName, @Nullable InternetDomainName tld) {
super(domainName, tld, "Domain");
}
@Override
WhoisResponse getSuccessResponse(DomainResource domain, DateTime now) {
return new DomainWhoisResponse(domain, now);
}
}
@@ -0,0 +1,70 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.domain.registry.model.EppResourceUtils.loadByUniqueId;
import static com.google.domain.registry.model.registry.Registries.findTldForName;
import static com.google.domain.registry.model.registry.Registries.getTlds;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.net.InternetDomainName;
import com.google.domain.registry.model.EppResource;
import com.google.domain.registry.util.TypeUtils.TypeInstantiator;
import org.joda.time.DateTime;
import javax.annotation.Nullable;
/** Represents a WHOIS lookup on a domain name (i.e. SLD) or a nameserver. */
abstract class DomainOrHostLookupCommand<T extends EppResource> implements WhoisCommand {
@VisibleForTesting
final InternetDomainName domainOrHostName;
private final String errorPrefix;
private Optional<InternetDomainName> tld;
DomainOrHostLookupCommand(
InternetDomainName domainName, @Nullable InternetDomainName tld, String errorPrefix) {
this.errorPrefix = errorPrefix;
this.domainOrHostName = checkNotNull(domainName, "domainOrHostName");
this.tld = Optional.fromNullable(tld);
}
@Override
public final WhoisResponse executeQuery(final DateTime now) throws WhoisException {
if (!tld.isPresent()) {
tld = findTldForName(domainOrHostName);
}
// Google Policy: Do not return records under TLDs for which we're not authoritative.
if (tld.isPresent() && getTlds().contains(tld.get().toString())) {
T domainOrHost = loadByUniqueId(
new TypeInstantiator<T>(getClass()){}.getExactType(),
domainOrHostName.toString(),
now);
if (domainOrHost != null) {
return getSuccessResponse(domainOrHost, now);
}
}
throw new WhoisException(now, SC_NOT_FOUND, errorPrefix + " not found.");
}
/** Renders a response record, provided its successfully retrieved datastore entity. */
abstract WhoisResponse getSuccessResponse(T domainOrHost, DateTime now);
}
@@ -0,0 +1,180 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.collect.Iterables.tryFind;
import static com.google.domain.registry.util.CollectionUtils.isNullOrEmpty;
import static com.google.domain.registry.xml.UtcDateTimeAdapter.getFormattedString;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.model.contact.ContactPhoneNumber;
import com.google.domain.registry.model.contact.ContactResource;
import com.google.domain.registry.model.contact.PostalInfo;
import com.google.domain.registry.model.domain.DesignatedContact;
import com.google.domain.registry.model.domain.DesignatedContact.Type;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.GracePeriod;
import com.google.domain.registry.model.domain.ReferenceUnion;
import com.google.domain.registry.model.eppcommon.StatusValue;
import com.google.domain.registry.model.host.HostResource;
import com.google.domain.registry.model.registrar.Registrar;
import com.google.domain.registry.model.translators.EnumToAttributeAdapter.EppEnum;
import com.google.domain.registry.util.FormattingLogger;
import org.joda.time.DateTime;
import java.util.Set;
import javax.annotation.Nullable;
/** Represents a WHOIS response to a domain query. */
final class DomainWhoisResponse extends WhoisResponseImpl {
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
/** Prefix for status value URLs. */
private static final String ICANN_STATUS_URL_PREFIX = "https://www.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";
/** Domain which was the target of this WHOIS command. */
private final DomainResource domain;
/** Creates new WHOIS domain response on the given domain. */
DomainWhoisResponse(DomainResource domain, DateTime timestamp) {
super(timestamp);
this.domain = checkNotNull(domain, "domain");
}
@Override
public String getPlainTextOutput(final boolean preferUnicode) {
Registrar registrar = getRegistrar(domain.getCurrentSponsorClientId());
return new DomainEmitter()
.emitField("Domain Name",
maybeFormatHostname(domain.getFullyQualifiedDomainName(), preferUnicode))
.emitField("Registry Domain ID", domain.getRepoId())
.emitField("Registrar WHOIS Server", registrar.getWhoisServer())
.emitField("Registrar URL", registrar.getReferralUrl())
.emitField("Updated Date", getFormattedString(domain.getLastEppUpdateTime()))
.emitField("Creation Date", getFormattedString(domain.getCreationTime()))
.emitField("Registrar Registration Expiration Date",
getFormattedString(domain.getRegistrationExpirationTime()))
.emitField("Registrar", registrar.getRegistrarName())
.emitField("Sponsoring Registrar IANA ID",
registrar.getIanaIdentifier() == null ? null : registrar.getIanaIdentifier().toString())
.emitStatusValues(domain.getStatusValues(), domain.getGracePeriods())
.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.loadNameservers(),
new Function<HostResource, String>() {
@Override
public String apply(HostResource host) {
return maybeFormatHostname(host.getFullyQualifiedHostName(), preferUnicode);
}})
.emitField("DNSSEC", isNullOrEmpty(domain.getDsData()) ? "unsigned" : "signedDelegation")
.emitAwipMessage()
.emitFooter(getTimestamp())
.toString();
}
/** Returns the contact of the given type, or null if it does not exist. */
@Nullable
private ReferenceUnion<ContactResource> getContactReference(final Type type) {
Optional<DesignatedContact> contactOfType = tryFind(domain.getContacts(),
new Predicate<DesignatedContact>() {
@Override
public boolean apply(DesignatedContact d) {
return d.getType() == type;
}});
return contactOfType.isPresent() ? contactOfType.get().getContactId() : null;
}
/** Output emitter with logic for domains. */
class DomainEmitter extends Emitter<DomainEmitter> {
DomainEmitter emitPhone(
String contactType, String title, @Nullable ContactPhoneNumber phoneNumber) {
return emitField(
contactType, title, phoneNumber != null ? phoneNumber.getPhoneNumber() : null)
.emitField(
contactType, title, "Ext", phoneNumber != null ? phoneNumber.getExtension() : null);
}
/** Emit the contact entry of the given type. */
DomainEmitter emitContact(
String contactType,
@Nullable ReferenceUnion<ContactResource> contact,
boolean preferUnicode) {
if (contact == null) {
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.
ContactResource contactResource = contact.getLinked().get();
if (contactResource == null) {
logger.severefmt("(BUG) Broken reference found from domain %s to contact %s",
domain.getFullyQualifiedDomainName(), contact.getLinked());
return this;
}
emitField("Registry " + contactType, "ID", contactResource.getContactId());
PostalInfo postalInfo = chooseByUnicodePreference(
preferUnicode,
contactResource.getLocalizedPostalInfo(),
contactResource.getInternationalizedPostalInfo());
if (postalInfo != null) {
emitField(contactType, "Name", postalInfo.getName());
emitField(contactType, "Organization", postalInfo.getOrg());
emitAddress(contactType, postalInfo.getAddress());
}
return emitPhone(contactType, "Phone", contactResource.getVoiceNumber())
.emitPhone(contactType, "Fax", contactResource.getFaxNumber())
.emitField(contactType, "Email", contactResource.getEmailAddress());
}
/** 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(),
new Function<EppEnum, String>() {
@Override
public String apply(EppEnum 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);
}
}
}
@@ -0,0 +1,39 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import com.google.common.net.InternetDomainName;
import com.google.domain.registry.model.host.HostResource;
import org.joda.time.DateTime;
import javax.annotation.Nullable;
/** Represents a WHOIS lookup on a nameserver based on its hostname. */
final class NameserverLookupByHostCommand extends DomainOrHostLookupCommand<HostResource> {
NameserverLookupByHostCommand(InternetDomainName hostName) {
this(hostName, null);
}
NameserverLookupByHostCommand(InternetDomainName hostName, @Nullable InternetDomainName tld) {
super(hostName, tld, "Nameserver");
}
@Override
WhoisResponse getSuccessResponse(HostResource host, DateTime now) {
return new NameserverWhoisResponse(host, now);
}
}
@@ -0,0 +1,67 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.domain.registry.model.EppResourceUtils.queryNotDeleted;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.net.InternetDomainName;
import com.google.domain.registry.model.host.HostResource;
import com.google.domain.registry.model.registry.Registries;
import org.joda.time.DateTime;
import java.net.InetAddress;
/**
* 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
public WhoisResponse executeQuery(DateTime now) throws WhoisException {
ImmutableList<HostResource> hosts = FluentIterable
.from(queryNotDeleted(HostResource.class, now, "inetAddresses", ipAddress))
.filter(new Predicate<HostResource>() {
@Override
public boolean apply(final HostResource host) {
return Registries
.findTldForName(InternetDomainName.from(host.getFullyQualifiedHostName()))
.isPresent();
}})
.toList();
if (hosts.isEmpty()) {
throw new WhoisException(now, SC_NOT_FOUND, "No nameservers found.");
}
return new NameserverWhoisResponse(hosts, now);
}
}
@@ -0,0 +1,67 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.net.InetAddresses;
import com.google.domain.registry.model.host.HostResource;
import com.google.domain.registry.model.registrar.Registrar;
import org.joda.time.DateTime;
import java.net.InetAddress;
/** 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<HostResource> hosts;
/** Creates new WHOIS nameserver response on the given host. */
NameserverWhoisResponse(HostResource host, DateTime timestamp) {
this(ImmutableList.of(checkNotNull(host, "host")), timestamp);
}
/** Creates new WHOIS nameserver response on the given list of hosts. */
NameserverWhoisResponse(ImmutableList<HostResource> hosts, DateTime timestamp) {
super(timestamp);
this.hosts = checkNotNull(hosts, "hosts");
}
@Override
public String getPlainTextOutput(boolean preferUnicode) {
BasicEmitter emitter = new BasicEmitter();
for (HostResource host : hosts) {
Registrar registrar = getRegistrar(host.getCurrentSponsorClientId());
emitter
.emitField("Server Name", maybeFormatHostname(
host.getFullyQualifiedHostName(), preferUnicode))
.emitSet("IP Address", host.getInetAddresses(),
new Function<InetAddress, String>() {
@Override
public String apply(InetAddress addr) {
return InetAddresses.toAddrString(addr);
}})
.emitField("Registrar", registrar.getRegistrarName())
.emitField("Registrar WHOIS Server", registrar.getWhoisServer())
.emitField("Registrar URL", registrar.getReferralUrl())
.emitNewline();
}
return emitter.emitFooter(getTimestamp()).toString();
}
}
@@ -0,0 +1,105 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.domain.registry.util.CacheUtils.memoizeWithShortExpiration;
import static com.google.domain.registry.util.RegistrarUtils.normalizeRegistrarName;
import static javax.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.ImmutableMap;
import com.google.domain.registry.model.registrar.Registrar;
import com.google.domain.registry.util.FormattingLogger;
import org.joda.time.DateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** Represents a WHOIS lookup for a registrar by its name. */
final class RegistrarLookupCommand implements WhoisCommand {
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
/**
* 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(new Supplier<Map<String, Registrar>>() {
@Override
public Map<String, Registrar> get() {
Map<String, Registrar> map = new HashMap<>();
// Use the normalized registrar name as a key.
Iterable<Registrar> registrars = Registrar.loadAllActiveAndPubliclyVisible();
for (Registrar registrar : registrars) {
if (registrar.getRegistrarName() == null) {
continue;
}
String normalized = normalizeRegistrarName(registrar.getRegistrarName());
if (map.put(normalized, registrar) != null) {
logger.warning(normalized
+ " appeared as a normalized registrar name for more than one registrar");
}
}
// 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 : registrars) {
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)));
if (!map.containsKey(normalized)) {
map.put(normalized, registrar);
}
}
}
return ImmutableMap.copyOf(map);
}});
@VisibleForTesting
final String registrarName;
RegistrarLookupCommand(String registrarName) {
checkArgument(!isNullOrEmpty(registrarName), "registrarName");
this.registrarName = registrarName;
}
@Override
public WhoisResponse executeQuery(DateTime now) throws WhoisException {
Registrar registrar =
REGISTRAR_BY_NORMALIZED_NAME_CACHE.get().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);
}
}
@@ -0,0 +1,97 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.domain.registry.model.registrar.Registrar;
import com.google.domain.registry.model.registrar.RegistrarContact;
import org.joda.time.DateTime;
import java.util.Set;
import javax.annotation.Nullable;
/** 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 String getPlainTextOutput(boolean preferUnicode) {
Set<RegistrarContact> contacts = registrar.getContacts();
return new RegistrarEmitter()
.emitField("Registrar Name", registrar.getRegistrarName())
.emitAddress(null, chooseByUnicodePreference(
preferUnicode,
registrar.getLocalizedAddress(),
registrar.getInternationalizedAddress()))
.emitPhonesAndEmail(
registrar.getPhoneNumber(),
registrar.getFaxNumber(),
registrar.getEmailAddress())
.emitField("Registrar WHOIS Server", registrar.getWhoisServer())
.emitField("Registrar URL", registrar.getReferralUrl())
.emitRegistrarContacts("Admin", contacts, AdminOrTech.ADMIN)
.emitRegistrarContacts("Technical", contacts, AdminOrTech.TECH)
.emitFooter(getTimestamp())
.toString();
}
/** An emitter with logic for registrars. */
class RegistrarEmitter extends Emitter<RegistrarEmitter> {
/** Emits the registrar contact of the given type. */
RegistrarEmitter emitRegistrarContacts(
String contactLabel,
Iterable<RegistrarContact> contacts,
AdminOrTech type) {
for (RegistrarContact 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);
}
}
}
@@ -0,0 +1,51 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import com.google.domain.registry.util.Clock;
import org.joda.time.DateTime;
import java.io.IOException;
import java.io.StringReader;
import javax.inject.Inject;
/** High-level WHOIS API for other packages. */
public final class Whois {
private final Clock clock;
@Inject
public Whois(Clock clock) {
this.clock = clock;
}
/**
* Performs a WHOIS lookup on a plaintext query string.
*
* @throws WhoisException if the record is not found or the query is invalid
*/
public WhoisResponse lookup(String query) throws WhoisException {
DateTime now = clock.nowUtc();
try {
return new WhoisReader(new StringReader(query), now)
.readCommand()
.executeQuery(now);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,29 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import org.joda.time.DateTime;
/** Represents a WHOIS command request from a client. */
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;
}
@@ -0,0 +1,71 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import org.joda.time.DateTime;
import javax.annotation.Nullable;
/** 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 String getPlainTextOutput(boolean preferUnicode) {
String footer = new WhoisResponseImpl.BasicEmitter()
.emitNewline()
.emitFooter(getTimestamp())
.toString();
return getMessage() + footer;
}
}
@@ -0,0 +1,178 @@
// Copyright 2016 Google Inc. 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 com.google.domain.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 javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.domain.registry.config.ConfigModule.Config;
import com.google.domain.registry.request.Action;
import com.google.domain.registry.request.RequestPath;
import com.google.domain.registry.request.Response;
import com.google.domain.registry.util.Clock;
import com.google.domain.registry.util.FormattingLogger;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import javax.inject.Inject;
/**
* Human-Friendly HTTP WHOIS API
*
* <p>This API uses easy to understand paths rather than {@link WhoisServer} 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
* servlet is just going to replace the slashes with spaces and let {@link WhoisReader}
* figure out what to do.
*
* <p>This servlet 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>
* <p>
* <input id="query-input" placeholder="Domain, Nameserver, IP, etc." autofocus>
* <button id="search-button">Lookup</button>
* <p>
* <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 WhoisServer
*/
@Action(path = WhoisHttpServer.PATH, isPrefix = true)
public final class WhoisHttpServer implements Runnable {
public static final String PATH = "/whois/";
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
/**
* 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("whoisHttpExpires") Duration expires;
@Inject @RequestPath String requestPath;
@Inject WhoisHttpServer() {}
@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 command = decode(JOINER.join(SLASHER.split(path.substring(PATH.length())))) + "\r\n";
Reader reader = new StringReader(command);
DateTime now = clock.nowUtc();
sendResponse(SC_OK, new WhoisReader(reader, now).readCommand().executeQuery(now));
} catch (WhoisException e) {
sendResponse(e.getStatus(), e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void sendResponse(int status, WhoisResponse whoisResponse) {
response.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);
response.setPayload(whoisResponse.getPlainTextOutput(true));
}
/** 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.infofmt("Malformed WHOIS request path: %s (%s)", requestPath, pathData);
throw new WhoisException(clock.nowUtc(), SC_BAD_REQUEST, "Malformed path query.");
}
}
}
@@ -0,0 +1,47 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import dagger.Module;
import dagger.Provides;
import java.io.IOException;
import java.io.Reader;
import javax.servlet.http.HttpServletRequest;
/**
* Dagger module for the whois package.
*
* <h3>Dependencies</h3>
*
* <ul>
* <li>{@link com.google.domain.registry.request.RequestModule RequestModule}
* </ul>
*
* @see "com.google.domain.registry.module.frontend.FrontendComponent"
*/
@Module
public final class WhoisModule {
@Provides
static Reader provideHttpInputReader(HttpServletRequest req) {
try {
return req.getReader();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,212 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.domain.registry.model.registry.Registries.findTldForName;
import static com.google.domain.registry.util.DomainNameUtils.canonicalizeDomainName;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.io.CharStreams;
import com.google.common.net.InetAddresses;
import com.google.common.net.InternetDomainName;
import org.joda.time.DateTime;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
/**
* 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 {
/**
* 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 Reader reader;
private final DateTime now;
/** Creates a new WhoisReader that extracts its command from the specified Reader. */
WhoisReader(Reader reader, DateTime now) {
this.reader = checkNotNull(reader, "reader");
this.now = checkNotNull(now, "now");
}
/**
* 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() throws IOException, WhoisException {
return parseCommand(CharStreams.toString(reader));
}
/**
* 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) 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 {
return new DomainLookupCommand(InternetDomainName.from(
canonicalizeDomainName(tokens.get(1))));
} 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 {
return new NameserverLookupByIpCommand(InetAddresses.forString(tokens.get(1)));
} catch (IllegalArgumentException iae) {
// Silently ignore this exception.
}
// Try to parse the argument as a host name.
try {
return new NameserverLookupByHostCommand(InternetDomainName.from(
canonicalizeDomainName(tokens.get(1))));
} catch (IllegalArgumentException iae) {
// Silently ignore this exception.
}
// If we can't interpret the argument as either a host name or IP address, return an error.
throw new WhoisException(now, SC_BAD_REQUEST, String.format(
"Could not parse argument to '%s' command", NAMESERVER_LOOKUP_COMMAND));
}
// Check if the first token is equal to the registrar lookup command.
if (arg1.equalsIgnoreCase(REGISTRAR_LOOKUP_COMMAND)) {
if (tokens.size() == 1) {
throw new WhoisException(now, SC_BAD_REQUEST, String.format(
"Too few arguments to '%s' command.", REGISTRAR_LOOKUP_COMMAND));
}
return new RegistrarLookupCommand(Joiner.on(' ').join(tokens.subList(1, tokens.size())));
}
// If we have a single token, then try to interpret that in various ways.
if (tokens.size() == 1) {
// Try to parse it as an IP address. If successful, then this is a lookup on a nameserver.
try {
return new NameserverLookupByIpCommand(InetAddresses.forString(arg1));
} catch (IllegalArgumentException iae) {
// Silently ignore this exception.
}
// Try to parse it as a domain name or host name.
try {
final InternetDomainName targetName = InternetDomainName.from(canonicalizeDomainName(arg1));
// We don't know at this point whether we have a domain name or a host name. We have to
// search through our configured TLDs to see if there's one that prefixes the name.
Optional<InternetDomainName> tld = findTldForName(targetName);
if (!tld.isPresent()) {
// This target is not under any configured TLD, so just try it as a registrar name.
return new RegistrarLookupCommand(arg1);
}
// If the target is exactly one level above the TLD, then this is an second level domain
// (SLD) and we should do a domain lookup on it.
if (targetName.parent().equals(tld.get())) {
return new DomainLookupCommand(targetName, tld.get());
}
// The target is more than one level above the TLD, so we'll assume it's a nameserver.
return new NameserverLookupByHostCommand(targetName, tld.get());
} 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.
return new RegistrarLookupCommand(Joiner.on(' ').join(tokens));
}
/** 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;
}
}
@@ -0,0 +1,37 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import org.joda.time.DateTime;
/** Representation of a WHOIS query response. */
public interface WhoisResponse {
/**
* Returns a plain text 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}.
*/
String getPlainTextOutput(boolean preferUnicode);
/**
* Returns the time at which this response was created.
*/
DateTime getTimestamp();
}
@@ -0,0 +1,216 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.html.HtmlEscapers.htmlEscaper;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Supplier;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.google.common.io.Resources;
import com.google.domain.registry.model.eppcommon.Address;
import com.google.domain.registry.model.registrar.Registrar;
import com.google.domain.registry.util.Idn;
import com.google.domain.registry.xml.UtcDateTimeAdapter;
import org.joda.time.DateTime;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
/** Base class for responses to WHOIS queries. */
abstract class WhoisResponseImpl implements WhoisResponse {
/** Legal disclaimer that is appended to all WHOIS responses. */
private static final String DISCLAIMER = load("disclaimer.txt");
/** 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 Data Problem Reporting System";
/** ICANN problem reporting URL appended to all WHOIS responses. */
private static final String ICANN_REPORTING_URL = "http://wdprs.internic.net/";
private static final Registrar EMPTY_REGISTRAR = new Supplier<Registrar>() {
@Override
public Registrar get() {
// Use Type.TEST here to avoid requiring an IANA ID (the type does not appear in WHOIS).
return new Registrar.Builder().setType(Registrar.Type.TEST).build();
}}.get();
/** 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.fromNullable(localized).or(Optional.fromNullable(internationalized)).orNull();
} else {
return Optional.fromNullable(internationalized).or(Optional.fromNullable(localized)).orNull();
}
}
/** 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, FluentIterable
.from(values)
.transform(transform)
.toSortedList(Ordering.natural()));
}
/** Helper method that loops over a list of values and calls {@link #emitField}. */
E emitList(String title, Iterable<String> values) {
for (String value : values) {
emitField(title, value);
}
return thisCastToDerived();
}
/** Emit the field name and value followed by a newline. */
E emitField(String name, @Nullable String value) {
stringBuilder.append(cleanse(name)).append(':');
if (!isNullOrEmpty(value)) {
stringBuilder.append(' ').append(cleanse(value));
}
return emitNewline();
}
/** Emit a multi-part field name and value followed by a newline. */
E emitField(String... namePartsAndValue) {
List<String> parts = Arrays.asList(namePartsAndValue);
return emitField(
Joiner.on(' ').join(parts.subList(0, parts.size() - 1)), Iterables.getLast(parts));
}
/** Emit a contact address. */
E emitAddress(@Nullable String prefix, @Nullable Address address) {
prefix = isNullOrEmpty(prefix) ? "" : prefix + " ";
if (address != null) {
emitList(prefix + "Street", address.getStreet());
emitField(prefix + "City", address.getCity());
emitField(prefix + "State/Province", address.getState());
emitField(prefix + "Postal Code", address.getZip());
emitField(prefix + "Country", address.getCountryCode());
}
return thisCastToDerived();
}
/** Returns raw text that should be appended to the end of ALL WHOIS responses. */
E emitFooter(DateTime timestamp) {
emitField(ICANN_REPORTING_URL_FIELD, ICANN_REPORTING_URL);
// We are assuming that our WHOIS database is always completely up to date, since it's
// querying the live backend datastore.
stringBuilder.append(String.format(
">>> Last update of WHOIS database: %s <<<\r\n\r\n%s\r\n",
UtcDateTimeAdapter.getFormattedString(timestamp),
DISCLAIMER));
return thisCastToDerived();
}
/** Emits a string directly, followed by a newline. */
protected E emitRawLine(String string) {
stringBuilder.append(string);
return emitNewline();
}
/**
* Remove potentially dangerous stuff from WHOIS output fields.
*
* <ul>
* <li>Remove ASCII control characters like {@code \n} which could be used to forge output.
* <li>Escape HTML entities, just in case this gets injected poorly into a webpage.
* </ul>
*/
private String cleanse(String value) {
return htmlEscaper().escape(value).replaceAll("[\\x00-\\x1f]", " ");
}
@Override
public String toString() {
return stringBuilder.toString();
}
}
/** An emitter that needs no special logic. */
static class BasicEmitter extends Emitter<BasicEmitter> {}
/** Slurps UTF-8 file from jar, relative to this source file. */
private static String load(String relativeFilename) {
URL resource = Resources.getResource(WhoisResponseImpl.class, relativeFilename);
try {
return Resources.toString(resource, UTF_8).replaceAll("\r?\n", "\r\n").trim();
} catch (IOException e) {
throw new RuntimeException("Failed to slurp: " + relativeFilename, e);
}
}
/** Returns the registrar for this client id, or an empty registrar with null values. */
static Registrar getRegistrar(@Nullable String clientId) {
return Optional
.fromNullable(clientId == null ? null : Registrar.loadByClientId(clientId))
.or(EMPTY_REGISTRAR);
}
}
@@ -0,0 +1,88 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;
import static com.google.domain.registry.request.Action.Method.POST;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.net.MediaType;
import com.google.domain.registry.request.Action;
import com.google.domain.registry.request.Response;
import com.google.domain.registry.util.Clock;
import com.google.domain.registry.util.FormattingLogger;
import org.joda.time.DateTime;
import java.io.Reader;
import javax.inject.Inject;
/**
* 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 servlet 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 WhoisHttpServer
* @see <a href="http://www.ietf.org/rfc/rfc3912.txt">RFC 3912: WHOIS Protocol Specification</a>
*/
@Action(path = "/_dr/whois", method = POST)
public class WhoisServer implements Runnable {
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
/** 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 WhoisServer() {}
@Override
public void run() {
String responseText;
DateTime now = clock.nowUtc();
try {
responseText = new WhoisReader(input, now)
.readCommand()
.executeQuery(now)
.getPlainTextOutput(PREFER_UNICODE);
} catch (WhoisException e) {
responseText = e.getPlainTextOutput(PREFER_UNICODE);
} catch (Throwable t) {
logger.severe(t, "WHOIS request crashed");
responseText = "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);
}
}
@@ -0,0 +1,13 @@
WHOIS information is provided by Charleston Road Registry Inc. (CRR) solely for
query-based, informational purposes. By querying our WHOIS database, you are
agreeing to comply with these terms
(http://www.registry.google/about/whois-disclaimer.html) so please read them
carefully. 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 systems of CRR 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. CRR reserves the right to
restrict or deny your access to the Whois database, and may modify these terms
at any time.
@@ -0,0 +1,16 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.whois;