Skip to content

Commit

Permalink
Add BSA label to rdap-domain 404 responses for BSA domains
Browse files Browse the repository at this point in the history
  • Loading branch information
gbrodman committed Mar 5, 2025
1 parent fa54c26 commit 7700b3b
Show file tree
Hide file tree
Showing 17 changed files with 150 additions and 77 deletions.
12 changes: 6 additions & 6 deletions core/src/main/java/google/registry/model/tld/Tld.java
Original file line number Diff line number Diff line change
Expand Up @@ -199,16 +199,16 @@ public static Tld get(String tld) {

/** 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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
31 changes: 19 additions & 12 deletions core/src/main/java/google/registry/rdap/RdapActionBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -41,6 +42,7 @@
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;
Expand All @@ -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,
Expand All @@ -75,6 +81,7 @@ protected enum DeletedItemHandling {
@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 =
Expand Down Expand Up @@ -152,6 +159,10 @@ public void run() {
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());
Expand All @@ -166,8 +177,7 @@ public void run() {
}

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) {
Expand All @@ -176,21 +186,18 @@ void setError(int status, String title, String description) {
}
}

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()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ B addLink(Link link) {
*/
@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.
Expand Down
21 changes: 20 additions & 1 deletion core/src/main/java/google/registry/rdap/RdapDomainAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@
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;
Expand Down Expand Up @@ -51,8 +54,9 @@ public RdapDomain getJsonObjectForResource(String pathSearchString, boolean isHe
// 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(
Expand All @@ -65,7 +69,9 @@ public RdapDomain getJsonObjectForResource(String pathSearchString, boolean isHe
Domain.class,
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.
//
Expand All @@ -75,4 +81,17 @@ public RdapDomain getJsonObjectForResource(String pathSearchString, boolean isHe
}
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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,34 @@ 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
DOMAIN_STATUS_CODES_NOTICE,
// 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);

/**
Expand Down
49 changes: 37 additions & 12 deletions core/src/main/java/google/registry/rdap/RdapObjectClasses.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
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. */
Expand Down Expand Up @@ -137,10 +138,22 @@ Builder add(Vcard vcard) {
* 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;
}
}

/**
Expand Down Expand Up @@ -173,14 +186,7 @@ public abstract static class TopLevelReplyObject extends AbstractJsonableObject
@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) {
Expand Down Expand Up @@ -532,6 +538,25 @@ abstract static class Builder extends RdapNamedObjectBase.Builder<Builder> {
}
}

/** 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
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/google/registry/rdap/RdapResultSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* @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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -323,7 +323,8 @@ static <T extends EppResource> CriteriaQueryBuilder<T> queryItems(
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
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();

Expand Down
Loading

0 comments on commit 7700b3b

Please sign in to comment.