mirror of
https://github.com/google/nomulus
synced 2026-09-05 23:57:01 +00:00
Import code from internal repository to git
This commit is contained in:
@@ -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 <FQDN><dd>
|
||||
* Looks up the domain record for the fully qualified domain name.
|
||||
* <dt>nameserver <FQDN><dd>
|
||||
* Looks up the nameserver record for the fully qualified domain name.
|
||||
* <dt>nameserver <IP><dd>
|
||||
* Looks up the nameserver record at the given IP address.
|
||||
* <dt>registrar <IANA ID><dd>
|
||||
* Looks up the registrar record with the given IANA ID.
|
||||
* <dt>registrar <NAME><dd>
|
||||
* Looks up the registrar record with the given name.
|
||||
* <dt><IP><dd>
|
||||
* Looks up the nameserver record with the given IP address.
|
||||
* <dt><FQDN><dd>
|
||||
* Looks up the nameserver or domain record for the fully qualified domain name.
|
||||
* <dt><IANA ID><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;
|
||||
Reference in New Issue
Block a user