google-nomulus/java/com/google/domain/registry/whois/WhoisResponseImpl.java
cgoldfeder 4e6c8ec6fe Fix WHOIS formatting to match format from RA
Our whois format was flagged as wrong in the .meet PDT. Although
we had followed the AWIP samples from ICANN, the definitive list
of field names is from Specification 4 of our contract, available at
https://newgtlds.icann.org/sites/default/files/agreements/agreement-approved-09jan14-en.htm
and indeed our fields are incorrect. (The remaining formatting issues
are ambiguous but the PDT testers' interpretation is probably correct.)

Since the footer format is now somewhat more complicated, I also denormalized
the disclaimer field into all of the testdata files. (I spent some time
debugging an extra newline between the content and the disclaimer, and
it would have been far clearer to solve if the files had been this way.)
-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=120338930
2016-05-13 17:40:58 -04:00

222 lines
8.1 KiB
Java

// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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 emitLastUpdated(DateTime timestamp) {
// We are assuming that our WHOIS database is always completely up to date, since it's
// querying the live backend datastore.
stringBuilder
.append(">>> Last update of WHOIS database: ")
.append(UtcDateTimeAdapter.getFormattedString(timestamp))
.append(" <<<\r\n\r\n");
return thisCastToDerived();
}
/** Returns raw text that should be appended to the end of ALL WHOIS responses. */
E emitFooter() {
emitField(ICANN_REPORTING_URL_FIELD, ICANN_REPORTING_URL);
stringBuilder.append("\r\n").append(DISCLAIMER).append("\r\n");
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);
}
}