google-nomulus/java/google/registry/whois/WhoisResponseImpl.java
jianglai 643b30d31f Redact WHOIS output instead of removing the fields altogether
See https://www.icann.org/resources/pages/gtld-registration-data-specs-en/#appendixA for details on how certain fields are redacted.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=199295355
2018-06-06 15:14:38 -04:00

245 lines
8.9 KiB
Java

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