1
0
mirror of https://github.com/google/nomulus synced 2025-12-23 06:15:42 +00:00

Add BSA label to rdap-domain 404 responses for BSA domains (#2706)

This commit is contained in:
gbrodman
2025-03-07 08:58:18 -05:00
committed by GitHub
parent d2d43f4115
commit 6b0beeb477
17 changed files with 149 additions and 77 deletions

View File

@@ -199,16 +199,16 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
/** Returns the TLD entities for the given TLD strings, throwing if any don't exist. */
public static ImmutableSet<Tld> get(Set<String> tlds) {
Map<String, Optional<Tld>> registries = CACHE.getAll(tlds);
ImmutableSet<String> missingRegistries =
registries.entrySet().stream()
Map<String, Optional<Tld>> tldObjects = CACHE.getAll(tlds);
ImmutableSet<String> missingTlds =
tldObjects.entrySet().stream()
.filter(e -> e.getValue().isEmpty())
.map(Map.Entry::getKey)
.collect(toImmutableSet());
if (missingRegistries.isEmpty()) {
return registries.values().stream().map(Optional::get).collect(toImmutableSet());
if (missingTlds.isEmpty()) {
return tldObjects.values().stream().map(Optional::get).collect(toImmutableSet());
} else {
throw new TldNotFoundException(missingRegistries);
throw new TldNotFoundException(missingTlds);
}
}

View File

@@ -14,10 +14,10 @@
package google.registry.model.tld.label;
import static com.google.common.base.Charsets.US_ASCII;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.hash.Funnels.stringFunnel;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;

View File

@@ -13,8 +13,8 @@
// limitations under the License.
package google.registry.persistence.converter;
import static com.google.common.base.Charsets.US_ASCII;
import static com.google.common.hash.Funnels.stringFunnel;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.hash.BloomFilter;
import jakarta.persistence.AttributeConverter;

View File

@@ -14,14 +14,15 @@
package google.registry.rdap;
import static com.google.common.base.Charsets.UTF_8;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
import static google.registry.request.Actions.getPathForAction;
import static google.registry.util.DomainNameUtils.canonicalizeHostname;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
@@ -41,6 +42,7 @@ import google.registry.request.Parameter;
import google.registry.request.RequestMethod;
import google.registry.request.RequestPath;
import google.registry.request.Response;
import google.registry.util.Clock;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Optional;
@@ -60,6 +62,10 @@ public abstract class RdapActionBase implements Runnable {
private static final MediaType RESPONSE_MEDIA_TYPE =
MediaType.create("application", "rdap+json").withCharset(UTF_8);
private static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create();
private static final Gson FORMATTED_OUTPUT_GSON =
new GsonBuilder().disableHtmlEscaping().setPrettyPrinting().create();
/** Whether to include or exclude deleted items from a query. */
protected enum DeletedItemHandling {
EXCLUDE,
@@ -75,6 +81,7 @@ public abstract class RdapActionBase implements Runnable {
@Inject @Parameter("formatOutput") Optional<Boolean> formatOutputParam;
@Inject @Config("rdapResultSetMaxSize") int rdapResultSetMaxSize;
@Inject RdapMetrics rdapMetrics;
@Inject Clock clock;
/** Builder for metric recording. */
final RdapMetrics.RdapMetricInformation.Builder metricInformationBuilder =
@@ -152,6 +159,10 @@ public abstract class RdapActionBase implements Runnable {
response.setStatus(SC_OK);
setPayload(replyObject);
metricInformationBuilder.setStatusCode(SC_OK);
} catch (RdapDomainAction.DomainBlockedByBsaException e) {
logger.atInfo().withCause(e).log("Domain blocked by BSA");
setErrorCodes(SC_NOT_FOUND);
setPayload(new RdapObjectClasses.DomainBlockedByBsaErrorResponse(e.getMessage()));
} catch (HttpException e) {
logger.atInfo().withCause(e).log("Error in RDAP.");
setError(e.getResponseCode(), e.getResponseCodeString(), e.getMessage());
@@ -166,8 +177,7 @@ public abstract class RdapActionBase implements Runnable {
}
void setError(int status, String title, String description) {
metricInformationBuilder.setStatusCode(status);
response.setStatus(status);
setErrorCodes(status);
try {
setPayload(ErrorResponse.create(status, title, description));
} catch (Exception ex) {
@@ -176,21 +186,18 @@ public abstract class RdapActionBase implements Runnable {
}
}
void setErrorCodes(int status) {
metricInformationBuilder.setStatusCode(status);
response.setStatus(status);
}
void setPayload(ReplyPayloadBase replyObject) {
if (requestMethod == Action.Method.HEAD) {
return;
}
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.disableHtmlEscaping();
if (formatOutputParam.orElse(false)) {
gsonBuilder.setPrettyPrinting();
}
Gson gson = gsonBuilder.create();
TopLevelReplyObject topLevelObject =
TopLevelReplyObject.create(replyObject, rdapJsonFormatter.createTosNotice());
Gson gson = formatOutputParam.orElse(false) ? FORMATTED_OUTPUT_GSON : GSON;
response.setPayload(gson.toJson(topLevelObject.toJson()));
}

View File

@@ -119,7 +119,7 @@ final class RdapDataStructures {
*/
@AutoValue
@RestrictJsonNames("notices[]")
abstract static class Notice extends NoticeOrRemark {
public abstract static class Notice extends NoticeOrRemark {
/**
* Notice and Remark Type are defined in 10.2.1 of RFC 9083.

View File

@@ -20,8 +20,11 @@ import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.net.InternetDomainName;
import google.registry.flows.EppException;
import google.registry.flows.domain.DomainFlowUtils;
import google.registry.model.domain.Domain;
import google.registry.model.tld.Tld;
import google.registry.rdap.RdapJsonFormatter.OutputDataType;
import google.registry.rdap.RdapMetrics.EndpointType;
import google.registry.rdap.RdapObjectClasses.RdapDomain;
@@ -51,8 +54,9 @@ public class RdapDomainAction extends RdapActionBase {
// RDAP Technical Implementation Guide 2.1.1 - we must support A-label (Punycode) and U-label
// (Unicode) formats. canonicalizeName will transform Unicode to Punycode so we support both.
pathSearchString = canonicalizeName(pathSearchString);
InternetDomainName domainName;
try {
validateDomainName(pathSearchString);
domainName = validateDomainName(pathSearchString);
} catch (EppException e) {
throw new BadRequestException(
String.format(
@@ -66,6 +70,7 @@ public class RdapDomainAction extends RdapActionBase {
pathSearchString,
shouldIncludeDeleted() ? START_OF_TIME : rdapJsonFormatter.getRequestTime());
if (domain.isEmpty() || !isAuthorized(domain.get())) {
handlePossibleBsaBlock(domainName);
// RFC7480 5.3 - if the server wishes to respond that it doesn't have data satisfying the
// query, it MUST reply with 404 response code.
//
@@ -75,4 +80,17 @@ public class RdapDomainAction extends RdapActionBase {
}
return rdapJsonFormatter.createRdapDomain(domain.get(), OutputDataType.FULL);
}
private void handlePossibleBsaBlock(InternetDomainName domainName) {
Tld tld = Tld.get(domainName.parent().toString());
if (DomainFlowUtils.isBlockedByBsa(domainName.parts().getFirst(), tld, clock.nowUtc())) {
throw new DomainBlockedByBsaException(domainName + " blocked by BSA");
}
}
static class DomainBlockedByBsaException extends RuntimeException {
DomainBlockedByBsaException(String message) {
super(message);
}
}
}

View File

@@ -63,8 +63,21 @@ public class RdapIcannStandardInformation {
.build())
.build();
/** Not required, but provided when a domain is blocked by BSA. */
private static final Notice DOMAIN_BLOCKED_BY_BSA_NOTICE =
Notice.builder()
.setTitle("Blocked Domain")
.setDescription("This name has been blocked by a GlobalBlock service")
.addLink(
Link.builder()
.setRel("alternate")
.setHref("https://brandsafetyalliance.co")
.setType("text/html")
.build())
.build();
/** Boilerplate notices required by domain responses. */
static final ImmutableList<Notice> domainBoilerplateNotices =
static final ImmutableList<Notice> DOMAIN_BOILERPLATE_NOTICES =
ImmutableList.of(
CONFORMANCE_NOTICE,
// RDAP Response Profile 2.6.3
@@ -72,8 +85,12 @@ public class RdapIcannStandardInformation {
// RDAP Response Profile 2.11
INACCURACY_COMPLAINT_FORM_NOTICE);
/** Boilerplate notice for when a domain is blocked by BSA. */
static final ImmutableList<Notice> DOMAIN_BLOCKED_BY_BSA_BOILERPLATE_NOTICES =
ImmutableList.of(DOMAIN_BLOCKED_BY_BSA_NOTICE);
/** Boilerplate remarks required by nameserver and entity responses. */
static final ImmutableList<Notice> nameserverAndEntityBoilerplateNotices =
static final ImmutableList<Notice> NAMESERVER_AND_ENTITY_BOILERPLATE_NOTICES =
ImmutableList.of(CONFORMANCE_NOTICE);
/**

View File

@@ -39,6 +39,7 @@ import google.registry.rdap.RdapDataStructures.RdapConformance;
import google.registry.rdap.RdapDataStructures.RdapStatus;
import google.registry.rdap.RdapDataStructures.Remark;
import google.registry.util.Idn;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Optional;
/** Object Classes defined in RFC 9083 section 5. */
@@ -137,10 +138,22 @@ final class RdapObjectClasses {
* suppress them for other types of responses (e.g. help).
*/
public enum BoilerplateType {
DOMAIN,
NAMESERVER,
ENTITY,
OTHER
DOMAIN(RdapIcannStandardInformation.DOMAIN_BOILERPLATE_NOTICES),
DOMAIN_BLOCKED_BY_BSA(RdapIcannStandardInformation.DOMAIN_BLOCKED_BY_BSA_BOILERPLATE_NOTICES),
NAMESERVER(RdapIcannStandardInformation.NAMESERVER_AND_ENTITY_BOILERPLATE_NOTICES),
ENTITY(RdapIcannStandardInformation.NAMESERVER_AND_ENTITY_BOILERPLATE_NOTICES),
OTHER(ImmutableList.of());
@SuppressWarnings("ImmutableEnumChecker") // immutable lists are, in fact, immutable
private final ImmutableList<Notice> notices;
BoilerplateType(ImmutableList<Notice> notices) {
this.notices = notices;
}
public ImmutableList<Notice> getNotices() {
return notices;
}
}
/**
@@ -173,14 +186,7 @@ final class RdapObjectClasses {
@JsonableElement("notices[]") abstract Notice aTosNotice();
@JsonableElement("notices") ImmutableList<Notice> boilerplateNotices() {
return switch (aAreplyObject().boilerplateType) {
case DOMAIN -> RdapIcannStandardInformation.domainBoilerplateNotices;
case NAMESERVER, ENTITY ->
RdapIcannStandardInformation.nameserverAndEntityBoilerplateNotices;
default -> // things other than domains, nameservers and entities do not yet have
// boilerplate
ImmutableList.of();
};
return aAreplyObject().boilerplateType.getNotices();
}
static TopLevelReplyObject create(ReplyPayloadBase replyObject, Notice tosNotice) {
@@ -532,6 +538,25 @@ final class RdapObjectClasses {
}
}
/** Specialized error response body for when a domain is blocked by BSA. */
@RestrictJsonNames({})
@SuppressWarnings("UnusedVariable")
public static class DomainBlockedByBsaErrorResponse extends ReplyPayloadBase {
@JsonableElement private static final LanguageIdentifier lang = LanguageIdentifier.EN;
@JsonableElement private static final int errorCode = HttpServletResponse.SC_NOT_FOUND;
@JsonableElement private static final String title = "Not Found";
@JsonableElement private final ImmutableList<String> description;
DomainBlockedByBsaErrorResponse(String message) {
super(BoilerplateType.DOMAIN_BLOCKED_BY_BSA);
this.description = ImmutableList.of(message);
}
}
/** Error Response Body defined in 6 of RFC 9083. */
@RestrictJsonNames({})
@AutoValue

View File

@@ -27,7 +27,7 @@ import java.util.List;
* @param numResourcesRetrieved Number of resources retrieved from the database in the process of
* assembling the data set.
*/
record RdapResultSet<T extends EppResource>(
public record RdapResultSet<T extends EppResource>(
ImmutableList<T> resources,
IncompletenessWarningType incompletenessWarningType,
int numResourcesRetrieved) {

View File

@@ -14,9 +14,9 @@
package google.registry.rdap;
import static com.google.common.base.Charsets.UTF_8;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
@@ -69,7 +69,7 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
public final BaseSearchResponse getJsonObjectForResource(
String pathSearchString, boolean isHeadRequest) {
// The pathSearchString is not used by search commands.
if (pathSearchString.length() > 0) {
if (!pathSearchString.isEmpty()) {
throw new BadRequestException("Unexpected path");
}
decodeCursorToken();
@@ -323,7 +323,8 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
if (partialStringQuery.getHasWildcard()) {
builder =
builder.where(
filterField, criteriaBuilder::like,
filterField,
criteriaBuilder::like,
String.format("%s%%", partialStringQuery.getInitialString()));
} else {
// no wildcard means we use a standard equals query

View File

@@ -40,7 +40,7 @@ import java.util.Optional;
abstract class RdapSearchResults {
/** Responding To Searches defined in 8 of RFC 9083. */
abstract static class BaseSearchResponse extends ReplyPayloadBase {
public abstract static class BaseSearchResponse extends ReplyPayloadBase {
abstract IncompletenessWarningType incompletenessWarningType();
abstract ImmutableMap<String, URI> navigationLinks();

View File

@@ -20,6 +20,7 @@ import com.google.common.flogger.FluentLogger;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.logging.Level;
import javax.annotation.Nullable;
/** Base for exceptions that cause an HTTP error response. */
public abstract class HttpException extends RuntimeException {
@@ -33,13 +34,14 @@ public abstract class HttpException extends RuntimeException {
private final int responseCode;
protected HttpException(int responseCode, String message, Throwable cause, Level logLevel) {
protected HttpException(
int responseCode, String message, @Nullable Throwable cause, Level logLevel) {
super(message, cause);
this.responseCode = responseCode;
this.logLevel = logLevel;
}
protected HttpException(int responseCode, String message, Throwable cause) {
protected HttpException(int responseCode, String message, @Nullable Throwable cause) {
this(responseCode, message, cause, Level.INFO);
}
@@ -117,22 +119,6 @@ public abstract class HttpException extends RuntimeException {
}
}
/** Exception that causes a 403 response. */
public static final class ForbiddenException extends HttpException {
public ForbiddenException(String message) {
super(HttpServletResponse.SC_FORBIDDEN, message, null);
}
public ForbiddenException(String message, Exception cause) {
super(HttpServletResponse.SC_FORBIDDEN, message, cause);
}
@Override
public String getResponseCodeString() {
return "Forbidden";
}
}
/** Exception that causes a 404 response. */
public static final class NotFoundException extends HttpException {
public NotFoundException() {
@@ -149,18 +135,6 @@ public abstract class HttpException extends RuntimeException {
}
}
/** Exception that causes a 405 response. */
public static final class MethodNotAllowedException extends HttpException {
public MethodNotAllowedException() {
super(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "Method not allowed", null);
}
@Override
public String getResponseCodeString() {
return "Method Not Allowed";
}
}
/** Exception that causes a 409 response. */
public static final class ConflictException extends HttpException {
public ConflictException(String message) {

View File

@@ -13,11 +13,11 @@
// limitations under the License.
package google.registry.persistence.converter;
import static com.google.common.base.Charsets.US_ASCII;
import static com.google.common.hash.Funnels.stringFunnel;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.insertInDb;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.BloomFilter;

View File

@@ -21,7 +21,6 @@ import static google.registry.testing.DatabaseHelper.insertSimpleResources;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.testcontainers.containers.PostgreSQLContainer.POSTGRESQL_PORT;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
@@ -305,7 +304,7 @@ public abstract class JpaTransactionManagerExtension
private static String readSqlInClassPath(String sqlScriptPath) {
try {
return Resources.toString(Resources.getResource(sqlScriptPath), Charsets.UTF_8);
return Resources.toString(Resources.getResource(sqlScriptPath), UTF_8);
} catch (IOException e) {
throw new UncheckedIOException(e);
}

View File

@@ -85,6 +85,7 @@ abstract class RdapActionBaseTestCase<A extends RdapActionBase> {
action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(clock);
action.rdapMetrics = rdapMetrics;
action.requestMethod = GET;
action.clock = new FakeClock(DateTime.parse("2025-01-01T00:00:00.000Z"));
logout();
}

View File

@@ -15,6 +15,7 @@
package google.registry.rdap;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.bsa.persistence.BsaTestingUtils.persistBsaLabel;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
@@ -27,8 +28,11 @@ import static google.registry.testing.FullFieldsTestEntityHelper.makeHistoryEntr
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrar;
import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrarPocs;
import static google.registry.testing.GsonSubject.assertAboutJson;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.mockito.Mockito.verify;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gson.JsonObject;
import google.registry.model.contact.Contact;
@@ -608,6 +612,34 @@ class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainAction> {
.build());
}
@Test
void testBlockedByBsa() {
persistResource(
Tld.get("lol").asBuilder().setBsaEnrollStartTime(Optional.of(START_OF_TIME)).build());
persistBsaLabel("example");
ImmutableMap<?, ?> expectedBsaNotice =
ImmutableMap.of(
"description",
ImmutableList.of("This name has been blocked by a GlobalBlock service"),
"title",
"Blocked Domain",
"links",
ImmutableList.of(
ImmutableMap.of(
"href",
"https://brandsafetyalliance.co",
"rel",
"alternate",
"type",
"text/html")));
JsonObject expectedErrorResponse = generateExpectedJsonError("example.lol blocked by BSA", 404);
expectedErrorResponse
.getAsJsonArray("notices")
.add(RdapTestHelper.GSON.toJsonTree(expectedBsaNotice));
assertAboutJson().that(generateActualJson("example.lol")).isEqualTo(expectedErrorResponse);
assertThat(response.getStatus()).isEqualTo(404);
}
private Domain persistActiveDomainWithHost(
String label, String tld, DateTime creationTime, DateTime expirationTime) {
return persistResource(

View File

@@ -33,8 +33,7 @@ import java.util.Map;
/** Test helper methods for RDAP tests. */
class RdapTestHelper {
private static final Gson GSON =
new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
static JsonElement createJson(String... lines) {
return GSON.fromJson(Joiner.on("\n").join(lines), JsonElement.class);
@@ -240,5 +239,4 @@ class RdapTestHelper {
obj.remove("rdapConformance");
return reply;
}
}