mirror of
https://github.com/google/nomulus.git
synced 2025-05-14 00:17:20 +02:00
Conform to RDAP Technical Implementation Guide
------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=251864499
This commit is contained in:
parent
55dc735ba0
commit
ca1d525e28
65 changed files with 869 additions and 709 deletions
|
@ -17,9 +17,7 @@ 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.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.request.Actions.getPathForAction;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
|
@ -29,30 +27,22 @@ import com.google.common.flogger.FluentLogger;
|
|||
import com.google.common.net.MediaType;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.re2j.Pattern;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.cmd.Query;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.rdap.RdapMetrics.EndpointType;
|
||||
import google.registry.rdap.RdapMetrics.WildcardType;
|
||||
import google.registry.rdap.RdapObjectClasses.ErrorResponse;
|
||||
import google.registry.rdap.RdapObjectClasses.ReplyPayloadBase;
|
||||
import google.registry.rdap.RdapObjectClasses.TopLevelReplyObject;
|
||||
import google.registry.rdap.RdapSearchResults.BaseSearchResponse;
|
||||
import google.registry.rdap.RdapSearchResults.IncompletenessWarningType;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.HttpException;
|
||||
import google.registry.request.HttpException.UnprocessableEntityException;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.RequestMethod;
|
||||
import google.registry.request.RequestPath;
|
||||
import google.registry.request.Response;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
|
@ -67,13 +57,6 @@ public abstract class RdapActionBase implements Runnable {
|
|||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
/**
|
||||
* Pattern for checking LDH names, which must officially contains only alphanumeric plus dots and
|
||||
* hyphens. In this case, allow the wildcard asterisk as well.
|
||||
*/
|
||||
static final Pattern LDH_PATTERN = Pattern.compile("[-.a-zA-Z0-9*]+");
|
||||
private static final int RESULT_SET_SIZE_SCALING_FACTOR = 30;
|
||||
|
||||
private static final MediaType RESPONSE_MEDIA_TYPE =
|
||||
MediaType.create("application", "rdap+json").withCharset(UTF_8);
|
||||
|
||||
|
@ -88,7 +71,6 @@ public abstract class RdapActionBase implements Runnable {
|
|||
@Inject @RequestPath String requestPath;
|
||||
@Inject RdapAuthorization rdapAuthorization;
|
||||
@Inject RdapJsonFormatter rdapJsonFormatter;
|
||||
@Inject @Parameter("registrar") Optional<String> registrarParam;
|
||||
@Inject @Parameter("includeDeleted") Optional<Boolean> includeDeletedParam;
|
||||
@Inject @Parameter("formatOutput") Optional<Boolean> formatOutputParam;
|
||||
@Inject @Config("rdapResultSetMaxSize") int rdapResultSetMaxSize;
|
||||
|
@ -121,6 +103,8 @@ public abstract class RdapActionBase implements Runnable {
|
|||
/**
|
||||
* Does the actual search and returns an RDAP JSON object.
|
||||
*
|
||||
* RFC7480 4.1 - we have to support GET and HEAD.
|
||||
*
|
||||
* @param pathSearchString the search string in the URL path
|
||||
* @param isHeadRequest whether the returned map will actually be used. HTTP HEAD requests don't
|
||||
* actually return anything. However, we usually still want to go through the process of
|
||||
|
@ -135,30 +119,37 @@ public abstract class RdapActionBase implements Runnable {
|
|||
@Override
|
||||
public void run() {
|
||||
metricInformationBuilder.setIncludeDeleted(includeDeletedParam.orElse(false));
|
||||
metricInformationBuilder.setRegistrarSpecified(registrarParam.isPresent());
|
||||
metricInformationBuilder.setRole(rdapAuthorization.role());
|
||||
metricInformationBuilder.setRequestMethod(requestMethod);
|
||||
metricInformationBuilder.setEndpointType(endpointType);
|
||||
// RFC7480 4.2 - servers receiving an RDAP request return an entity with a Content-Type header
|
||||
// containing the RDAP-specific JSON media type.
|
||||
response.setContentType(RESPONSE_MEDIA_TYPE);
|
||||
// RDAP Technical Implementation Guide 1.13 - when responding to RDAP valid requests, we MUST
|
||||
// include the Access-Control-Allow-Origin, which MUST be "*" unless otherwise specified.
|
||||
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
|
||||
try {
|
||||
// Extract what we're searching for from the request path. Some RDAP commands use trailing
|
||||
// data in the path itself (e.g. /rdap/domain/mydomain.com), and some use the query string
|
||||
// (e.g. /rdap/domains?name=mydomain); the query parameters are extracted by the subclasses
|
||||
// directly as needed.
|
||||
response.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
|
||||
URI uri = new URI(requestPath);
|
||||
String pathProper = uri.getPath();
|
||||
checkArgument(
|
||||
pathProper.startsWith(getActionPath()),
|
||||
"%s doesn't start with %s", pathProper, getActionPath());
|
||||
String pathSearchString = pathProper.substring(getActionPath().length());
|
||||
logger.atInfo().log("path search string: '%s'", pathSearchString);
|
||||
|
||||
ReplyPayloadBase replyObject =
|
||||
getJsonObjectForResource(
|
||||
pathProper.substring(getActionPath().length()), requestMethod == Action.Method.HEAD);
|
||||
getJsonObjectForResource(pathSearchString, requestMethod == Action.Method.HEAD);
|
||||
if (replyObject instanceof BaseSearchResponse) {
|
||||
metricInformationBuilder.setIncompletenessWarningType(
|
||||
((BaseSearchResponse) replyObject).incompletenessWarningType());
|
||||
}
|
||||
// RFC7480 5.1 - if the server has the information requested and wishes to respond, it returns
|
||||
// that answer in the body of a 200 (OK) response
|
||||
response.setStatus(SC_OK);
|
||||
response.setContentType(RESPONSE_MEDIA_TYPE);
|
||||
setPayload(replyObject);
|
||||
metricInformationBuilder.setStatusCode(SC_OK);
|
||||
} catch (HttpException e) {
|
||||
|
@ -177,7 +168,6 @@ public abstract class RdapActionBase implements Runnable {
|
|||
void setError(int status, String title, String description) {
|
||||
metricInformationBuilder.setStatusCode(status);
|
||||
response.setStatus(status);
|
||||
response.setContentType(RESPONSE_MEDIA_TYPE);
|
||||
try {
|
||||
setPayload(ErrorResponse.create(status, title, description));
|
||||
} catch (Exception ex) {
|
||||
|
@ -204,11 +194,6 @@ public abstract class RdapActionBase implements Runnable {
|
|||
response.setPayload(gson.toJson(topLevelObject.toJson()));
|
||||
}
|
||||
|
||||
/** Returns the registrar on which results should be filtered, or absent(). */
|
||||
Optional<String> getDesiredRegistrar() {
|
||||
return registrarParam;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the query should include deleted items.
|
||||
*
|
||||
|
@ -247,43 +232,16 @@ public abstract class RdapActionBase implements Runnable {
|
|||
eppResource.getPersistedCurrentSponsorClientId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the EPP resource should be visible.
|
||||
*
|
||||
* <p>This is true iff: 1. The resource is not deleted, or the request wants to see deleted items,
|
||||
* and is authorized to do so, and: 2. The request did not specify a registrar to filter on, or
|
||||
* the registrar matches.
|
||||
*/
|
||||
boolean shouldBeVisible(EppResource eppResource) {
|
||||
return isAuthorized(eppResource)
|
||||
&& (!registrarParam.isPresent()
|
||||
|| registrarParam.get().equals(eppResource.getPersistedCurrentSponsorClientId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the EPP resource should be visible.
|
||||
*
|
||||
* <p>This is true iff: 1. The passed in resource exists and is not deleted (deleted ones will
|
||||
* have been projected forward in time to empty), 2. The request did not specify a registrar to
|
||||
* filter on, or the registrar matches.
|
||||
*/
|
||||
boolean shouldBeVisible(Optional<? extends EppResource> eppResource) {
|
||||
return eppResource.isPresent() && shouldBeVisible(eppResource.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the registrar should be visible.
|
||||
*
|
||||
* <p>This is true iff:
|
||||
* 1. The resource is active and publicly visible, or the request wants to see deleted items, and
|
||||
* is authorized to do so, and:
|
||||
* 2. The request did not specify a registrar to filter on, or the registrar matches.
|
||||
* <p>This is true iff: The resource is active and publicly visible, or the request wants to see
|
||||
* deleted items, and is authorized to do so
|
||||
*/
|
||||
boolean shouldBeVisible(Registrar registrar) {
|
||||
boolean isAuthorized(Registrar registrar) {
|
||||
return (registrar.isLiveAndPubliclyVisible()
|
||||
|| (shouldIncludeDeleted()
|
||||
&& rdapAuthorization.isAuthorizedForClientId(registrar.getClientId())))
|
||||
&& (!registrarParam.isPresent() || registrarParam.get().equals(registrar.getClientId()));
|
||||
&& rdapAuthorization.isAuthorizedForClientId(registrar.getClientId())));
|
||||
}
|
||||
|
||||
String canonicalizeName(String name) {
|
||||
|
@ -294,250 +252,6 @@ public abstract class RdapActionBase implements Runnable {
|
|||
return name;
|
||||
}
|
||||
|
||||
int getStandardQuerySizeLimit() {
|
||||
return shouldIncludeDeleted()
|
||||
? (RESULT_SET_SIZE_SCALING_FACTOR * (rdapResultSetMaxSize + 1))
|
||||
: (rdapResultSetMaxSize + 1);
|
||||
}
|
||||
|
||||
static <T extends EppResource> Query<T> queryItems(
|
||||
Class<T> clazz,
|
||||
String filterField,
|
||||
RdapSearchPattern partialStringQuery,
|
||||
DeletedItemHandling deletedItemHandling,
|
||||
int resultSetMaxSize) {
|
||||
return queryItems(
|
||||
clazz,
|
||||
filterField,
|
||||
partialStringQuery,
|
||||
Optional.empty(),
|
||||
deletedItemHandling,
|
||||
resultSetMaxSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles prefix searches in cases where, if we need to filter out deleted items, there are no
|
||||
* pending deletes.
|
||||
*
|
||||
* <p>In such cases, it is sufficient to check whether {@code deletionTime} is equal to
|
||||
* {@code END_OF_TIME}, because any other value means it has already been deleted. This allows us
|
||||
* to use an equality query for the deletion time.
|
||||
*
|
||||
* @param clazz the type of resource to be queried
|
||||
* @param filterField the database field of interest
|
||||
* @param partialStringQuery the details of the search string; if there is no wildcard, an
|
||||
* equality query is used; if there is a wildcard, a range query is used instead; the
|
||||
* initial string should not be empty, and any search suffix will be ignored, so the caller
|
||||
* must filter the results if a suffix is specified
|
||||
* @param cursorString if a cursor is present, this parameter should specify the cursor string, to
|
||||
* skip any results up to and including the string; empty() if there is no cursor
|
||||
* @param deletedItemHandling whether to include or exclude deleted items
|
||||
* @param resultSetMaxSize the maximum number of results to return
|
||||
* @return the query object
|
||||
*/
|
||||
static <T extends EppResource> Query<T> queryItems(
|
||||
Class<T> clazz,
|
||||
String filterField,
|
||||
RdapSearchPattern partialStringQuery,
|
||||
Optional<String> cursorString,
|
||||
DeletedItemHandling deletedItemHandling,
|
||||
int resultSetMaxSize) {
|
||||
if (partialStringQuery.getInitialString().length()
|
||||
< RdapSearchPattern.MIN_INITIAL_STRING_LENGTH) {
|
||||
throw new UnprocessableEntityException(
|
||||
String.format(
|
||||
"Initial search string must be at least %d characters",
|
||||
RdapSearchPattern.MIN_INITIAL_STRING_LENGTH));
|
||||
}
|
||||
Query<T> query = ofy().load().type(clazz);
|
||||
if (!partialStringQuery.getHasWildcard()) {
|
||||
query = query.filter(filterField, partialStringQuery.getInitialString());
|
||||
} else {
|
||||
// Ignore the suffix; the caller will need to filter on the suffix, if any.
|
||||
query = query
|
||||
.filter(filterField + " >=", partialStringQuery.getInitialString())
|
||||
.filter(filterField + " <", partialStringQuery.getNextInitialString());
|
||||
}
|
||||
if (cursorString.isPresent()) {
|
||||
query = query.filter(filterField + " >", cursorString.get());
|
||||
}
|
||||
return setOtherQueryAttributes(query, deletedItemHandling, resultSetMaxSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles searches using a simple string rather than an {@link RdapSearchPattern}.
|
||||
*
|
||||
* <p>Since the filter is not an inequality, we can support also checking a cursor string against
|
||||
* a different field (which involves an inequality on that field).
|
||||
*
|
||||
* @param clazz the type of resource to be queried
|
||||
* @param filterField the database field of interest
|
||||
* @param queryString the search string
|
||||
* @param cursorField the field which should be compared to the cursor string, or empty() if the
|
||||
* key should be compared to a key created from the cursor string
|
||||
* @param cursorString if a cursor is present, this parameter should specify the cursor string, to
|
||||
* skip any results up to and including the string; empty() if there is no cursor
|
||||
* @param deletedItemHandling whether to include or exclude deleted items
|
||||
* @param resultSetMaxSize the maximum number of results to return
|
||||
* @return the query object
|
||||
*/
|
||||
static <T extends EppResource> Query<T> queryItems(
|
||||
Class<T> clazz,
|
||||
String filterField,
|
||||
String queryString,
|
||||
Optional<String> cursorField,
|
||||
Optional<String> cursorString,
|
||||
DeletedItemHandling deletedItemHandling,
|
||||
int resultSetMaxSize) {
|
||||
if (queryString.length() < RdapSearchPattern.MIN_INITIAL_STRING_LENGTH) {
|
||||
throw new UnprocessableEntityException(
|
||||
String.format(
|
||||
"Initial search string must be at least %d characters",
|
||||
RdapSearchPattern.MIN_INITIAL_STRING_LENGTH));
|
||||
}
|
||||
Query<T> query = ofy().load().type(clazz).filter(filterField, queryString);
|
||||
if (cursorString.isPresent()) {
|
||||
if (cursorField.isPresent()) {
|
||||
query = query.filter(cursorField.get() + " >", cursorString.get());
|
||||
} else {
|
||||
query = query.filterKey(">", Key.create(clazz, cursorString.get()));
|
||||
}
|
||||
}
|
||||
return setOtherQueryAttributes(query, deletedItemHandling, resultSetMaxSize);
|
||||
}
|
||||
|
||||
/** Handles searches where the field to be searched is the key. */
|
||||
static <T extends EppResource> Query<T> queryItemsByKey(
|
||||
Class<T> clazz,
|
||||
RdapSearchPattern partialStringQuery,
|
||||
Optional<String> cursorString,
|
||||
DeletedItemHandling deletedItemHandling,
|
||||
int resultSetMaxSize) {
|
||||
if (partialStringQuery.getInitialString().length()
|
||||
< RdapSearchPattern.MIN_INITIAL_STRING_LENGTH) {
|
||||
throw new UnprocessableEntityException(
|
||||
String.format(
|
||||
"Initial search string must be at least %d characters",
|
||||
RdapSearchPattern.MIN_INITIAL_STRING_LENGTH));
|
||||
}
|
||||
Query<T> query = ofy().load().type(clazz);
|
||||
if (!partialStringQuery.getHasWildcard()) {
|
||||
query = query.filterKey("=", Key.create(clazz, partialStringQuery.getInitialString()));
|
||||
} else {
|
||||
// Ignore the suffix; the caller will need to filter on the suffix, if any.
|
||||
query = query
|
||||
.filterKey(">=", Key.create(clazz, partialStringQuery.getInitialString()))
|
||||
.filterKey("<", Key.create(clazz, partialStringQuery.getNextInitialString()));
|
||||
}
|
||||
if (cursorString.isPresent()) {
|
||||
query = query.filterKey(">", Key.create(clazz, cursorString.get()));
|
||||
}
|
||||
return setOtherQueryAttributes(query, deletedItemHandling, resultSetMaxSize);
|
||||
}
|
||||
|
||||
/** Handles searches by key using a simple string. */
|
||||
static <T extends EppResource> Query<T> queryItemsByKey(
|
||||
Class<T> clazz,
|
||||
String queryString,
|
||||
DeletedItemHandling deletedItemHandling,
|
||||
int resultSetMaxSize) {
|
||||
if (queryString.length() < RdapSearchPattern.MIN_INITIAL_STRING_LENGTH) {
|
||||
throw new UnprocessableEntityException(
|
||||
String.format(
|
||||
"Initial search string must be at least %d characters",
|
||||
RdapSearchPattern.MIN_INITIAL_STRING_LENGTH));
|
||||
}
|
||||
Query<T> query = ofy().load().type(clazz).filterKey("=", Key.create(clazz, queryString));
|
||||
return setOtherQueryAttributes(query, deletedItemHandling, resultSetMaxSize);
|
||||
}
|
||||
|
||||
private static <T extends EppResource> Query<T> setOtherQueryAttributes(
|
||||
Query<T> query, DeletedItemHandling deletedItemHandling, int resultSetMaxSize) {
|
||||
if (deletedItemHandling != DeletedItemHandling.INCLUDE) {
|
||||
query = query.filter("deletionTime", END_OF_TIME);
|
||||
}
|
||||
return query.limit(resultSetMaxSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given query, and checks for permissioning if necessary.
|
||||
*
|
||||
* @param query an already-defined query to be run; a filter on currentSponsorClientId will be
|
||||
* added if appropriate
|
||||
* @param checkForVisibility true if the results should be checked to make sure they are visible;
|
||||
* normally this should be equal to the shouldIncludeDeleted setting, but in cases where the
|
||||
* query could not check deletion status (due to Datastore limitations such as the limit of
|
||||
* one field queried for inequality, for instance), it may need to be set to true even when
|
||||
* not including deleted records
|
||||
* @param querySizeLimit the maximum number of items the query is expected to return, usually
|
||||
* because the limit has been set
|
||||
* @return an {@link RdapResultSet} object containing the list of resources and an incompleteness
|
||||
* warning flag, which is set to MIGHT_BE_INCOMPLETE iff any resources were excluded due to
|
||||
* lack of visibility, and the resulting list of resources is less than the maximum allowable,
|
||||
* and the number of items returned by the query is greater than or equal to the maximum
|
||||
* number we might have expected
|
||||
*/
|
||||
<T extends EppResource> RdapResultSet<T> getMatchingResources(
|
||||
Query<T> query, boolean checkForVisibility, int querySizeLimit) {
|
||||
Optional<String> desiredRegistrar = getDesiredRegistrar();
|
||||
if (desiredRegistrar.isPresent()) {
|
||||
query = query.filter("currentSponsorClientId", desiredRegistrar.get());
|
||||
}
|
||||
if (!checkForVisibility) {
|
||||
return RdapResultSet.create(query.list());
|
||||
}
|
||||
// If we are including deleted resources, we need to check that we're authorized for each one.
|
||||
List<T> resources = new ArrayList<>();
|
||||
int numResourcesQueried = 0;
|
||||
boolean someExcluded = false;
|
||||
for (T resource : query) {
|
||||
if (shouldBeVisible(resource)) {
|
||||
resources.add(resource);
|
||||
} else {
|
||||
someExcluded = true;
|
||||
}
|
||||
numResourcesQueried++;
|
||||
if (resources.size() > rdapResultSetMaxSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The incompleteness problem comes about because we don't know how many items to fetch. We want
|
||||
// to return rdapResultSetMaxSize worth of items, but some might be excluded, so we fetch more
|
||||
// just in case. But how many more? That's the potential problem, addressed with the three way
|
||||
// AND statement:
|
||||
// 1. If we didn't exclude any items, then we can't have the incompleteness problem.
|
||||
// 2. If have a full result set batch (rdapResultSetMaxSize items), we must by definition be
|
||||
// giving the user a complete result set.
|
||||
// 3. If we started with fewer than querySizeLimit items, then there weren't any more items that
|
||||
// we missed. Even if we return fewer than rdapResultSetMaxSize items, it isn't because we
|
||||
// didn't fetch enough to start.
|
||||
// Only if all three conditions are true might things be incomplete. In other words, we fetched
|
||||
// as many as our limit allowed, but then excluded so many that we wound up with less than a
|
||||
// full result set's worth of results.
|
||||
return RdapResultSet.create(
|
||||
resources,
|
||||
(someExcluded
|
||||
&& (resources.size() < rdapResultSetMaxSize)
|
||||
&& (numResourcesQueried >= querySizeLimit))
|
||||
? IncompletenessWarningType.MIGHT_BE_INCOMPLETE
|
||||
: IncompletenessWarningType.COMPLETE,
|
||||
numResourcesQueried);
|
||||
}
|
||||
|
||||
RdapSearchPattern recordWildcardType(RdapSearchPattern partialStringQuery) {
|
||||
if (!partialStringQuery.getHasWildcard()) {
|
||||
metricInformationBuilder.setWildcardType(WildcardType.NO_WILDCARD);
|
||||
} else if (partialStringQuery.getSuffix() == null) {
|
||||
metricInformationBuilder.setWildcardType(WildcardType.PREFIX);
|
||||
} else if (partialStringQuery.getInitialString().isEmpty()) {
|
||||
metricInformationBuilder.setWildcardType(WildcardType.SUFFIX);
|
||||
} else {
|
||||
metricInformationBuilder.setWildcardType(WildcardType.PREFIX_AND_SUFFIX);
|
||||
}
|
||||
metricInformationBuilder.setPrefixLength(partialStringQuery.getInitialString().length());
|
||||
return partialStringQuery;
|
||||
}
|
||||
|
||||
/** Returns the DateTime this request took place. */
|
||||
DateTime getRequestTime() {
|
||||
return rdapJsonFormatter.getRequestTime();
|
||||
|
|
|
@ -48,6 +48,11 @@ final class RdapDataStructures {
|
|||
// Conformance to the RDAP Response Profile V2.1
|
||||
// (see section 1.3)
|
||||
jsonArray.add("icann_rdap_response_profile_0");
|
||||
|
||||
// Conformance to the RDAP Technical Implementation Guide V2.1
|
||||
// (see section 1.14)
|
||||
jsonArray.add("icann_rdap_technical_implementation_guide_0");
|
||||
|
||||
return jsonArray;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,8 @@ public class RdapDomainAction extends RdapActionBase {
|
|||
|
||||
@Override
|
||||
public RdapDomain getJsonObjectForResource(String pathSearchString, boolean isHeadRequest) {
|
||||
// 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);
|
||||
try {
|
||||
validateDomainName(pathSearchString);
|
||||
|
@ -62,7 +64,12 @@ public class RdapDomainAction extends RdapActionBase {
|
|||
DomainBase.class,
|
||||
pathSearchString,
|
||||
shouldIncludeDeleted() ? START_OF_TIME : rdapJsonFormatter.getRequestTime());
|
||||
if (!shouldBeVisible(domainBase)) {
|
||||
if (!domainBase.isPresent() || !isAuthorized(domainBase.get())) {
|
||||
// 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.
|
||||
//
|
||||
// Note we don't do RFC7480 5.3 - returning a different code if we wish to say "this info
|
||||
// exists but we don't want to show it to you", because we DON'T wish to say that.
|
||||
throw new NotFoundException(pathSearchString + " not found");
|
||||
}
|
||||
return rdapJsonFormatter.createRdapDomain(domainBase.get(), OutputDataType.FULL);
|
||||
|
|
|
@ -44,7 +44,6 @@ import google.registry.request.HttpException.NotFoundException;
|
|||
import google.registry.request.HttpException.UnprocessableEntityException;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Idn;
|
||||
import google.registry.util.NonFinalForTesting;
|
||||
import java.net.InetAddress;
|
||||
import java.util.Comparator;
|
||||
|
@ -62,6 +61,9 @@ import javax.inject.Inject;
|
|||
* (RDAP) Query Format</a>
|
||||
* @see <a href="http://tools.ietf.org/html/rfc7483">RFC 7483: JSON Responses for the Registration
|
||||
* Data Access Protocol (RDAP)</a>
|
||||
*
|
||||
* TODO(guyben):This isn't required by the RDAP Technical Implementation Guide, and hence should be
|
||||
* deleted, at least until it's actually required.
|
||||
*/
|
||||
@Action(
|
||||
service = Action.Service.PUBAPI,
|
||||
|
@ -90,41 +92,29 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
|
|||
* <p>The RDAP spec allows for domain search by domain name, nameserver name or nameserver IP.
|
||||
*/
|
||||
@Override
|
||||
public DomainSearchResponse getJsonObjectForResource(
|
||||
String pathSearchString, boolean isHeadRequest) {
|
||||
public DomainSearchResponse getSearchResponse(boolean isHeadRequest) {
|
||||
// RDAP syntax example: /rdap/domains?name=exam*.com.
|
||||
// The pathSearchString is not used by search commands.
|
||||
if (pathSearchString.length() > 0) {
|
||||
throw new BadRequestException("Unexpected path");
|
||||
}
|
||||
if (Booleans.countTrue(nameParam.isPresent(), nsLdhNameParam.isPresent(), nsIpParam.isPresent())
|
||||
!= 1) {
|
||||
throw new BadRequestException(
|
||||
"You must specify either name=XXXX, nsLdhName=YYYY or nsIp=ZZZZ");
|
||||
}
|
||||
decodeCursorToken();
|
||||
DomainSearchResponse results;
|
||||
if (nameParam.isPresent()) {
|
||||
metricInformationBuilder.setSearchType(SearchType.BY_DOMAIN_NAME);
|
||||
// syntax: /rdap/domains?name=exam*.com
|
||||
String asciiName;
|
||||
try {
|
||||
asciiName = Idn.toASCII(nameParam.get());
|
||||
} catch (Exception e) {
|
||||
throw new BadRequestException("Invalid value of nsLdhName parameter");
|
||||
}
|
||||
results = searchByDomainName(recordWildcardType(RdapSearchPattern.create(asciiName, true)));
|
||||
results =
|
||||
searchByDomainName(
|
||||
recordWildcardType(
|
||||
RdapSearchPattern.createFromLdhOrUnicodeDomainName(nameParam.get())));
|
||||
} else if (nsLdhNameParam.isPresent()) {
|
||||
metricInformationBuilder.setSearchType(SearchType.BY_NAMESERVER_NAME);
|
||||
// syntax: /rdap/domains?nsLdhName=ns1.exam*.com
|
||||
// RFC 7482 appears to say that Unicode domains must be specified using punycode when
|
||||
// passed to nsLdhName, so IDN.toASCII is not called here.
|
||||
if (!LDH_PATTERN.matcher(nsLdhNameParam.get()).matches()) {
|
||||
throw new BadRequestException("Invalid value of nsLdhName parameter");
|
||||
}
|
||||
results =
|
||||
searchByNameserverLdhName(
|
||||
recordWildcardType(RdapSearchPattern.create(nsLdhNameParam.get(), true)));
|
||||
recordWildcardType(RdapSearchPattern.createFromLdhDomainName(nsLdhNameParam.get())));
|
||||
} else {
|
||||
metricInformationBuilder.setSearchType(SearchType.BY_NAMESERVER_ADDRESS);
|
||||
metricInformationBuilder.setWildcardType(WildcardType.NO_WILDCARD);
|
||||
|
@ -189,7 +179,9 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
|
|||
Optional<DomainBase> domainBase =
|
||||
loadByForeignKey(DomainBase.class, partialStringQuery.getInitialString(), getRequestTime());
|
||||
return makeSearchResults(
|
||||
shouldBeVisible(domainBase) ? ImmutableList.of(domainBase.get()) : ImmutableList.of());
|
||||
shouldBeVisible(domainBase)
|
||||
? ImmutableList.of(domainBase.get())
|
||||
: ImmutableList.of());
|
||||
}
|
||||
|
||||
/** Searches for domains by domain name with an initial string, wildcard and possible suffix. */
|
||||
|
@ -294,6 +286,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
|
|||
HostResource.class,
|
||||
"fullyQualifiedHostName",
|
||||
partialStringQuery,
|
||||
Optional.empty(),
|
||||
DeletedItemHandling.EXCLUDE,
|
||||
maxNameserversInFirstStage);
|
||||
Optional<String> desiredRegistrar = getDesiredRegistrar();
|
||||
|
|
|
@ -16,6 +16,7 @@ package google.registry.rdap;
|
|||
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.rdap.RdapUtils.getRegistrarByIanaIdentifier;
|
||||
import static google.registry.rdap.RdapUtils.getRegistrarByName;
|
||||
import static google.registry.request.Action.Method.GET;
|
||||
import static google.registry.request.Action.Method.HEAD;
|
||||
|
||||
|
@ -29,7 +30,6 @@ import google.registry.rdap.RdapJsonFormatter.OutputDataType;
|
|||
import google.registry.rdap.RdapMetrics.EndpointType;
|
||||
import google.registry.rdap.RdapObjectClasses.RdapEntity;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.HttpException.BadRequestException;
|
||||
import google.registry.request.HttpException.NotFoundException;
|
||||
import google.registry.request.auth.Auth;
|
||||
import java.util.Optional;
|
||||
|
@ -63,31 +63,46 @@ public class RdapEntityAction extends RdapActionBase {
|
|||
public RdapEntity getJsonObjectForResource(
|
||||
String pathSearchString, boolean isHeadRequest) {
|
||||
// The query string is not used; the RDAP syntax is /rdap/entity/handle (the handle is the roid
|
||||
// for contacts and the client identifier for registrars). Since RDAP's concept of an entity
|
||||
// for contacts and the client identifier/fn for registrars). Since RDAP's concept of an entity
|
||||
// includes both contacts and registrars, search for one first, then the other.
|
||||
boolean wasValidKey = false;
|
||||
|
||||
// RDAP Technical Implementation Guide 2.3.1 - MUST support contact entity lookup using the
|
||||
// handle
|
||||
if (ROID_PATTERN.matcher(pathSearchString).matches()) {
|
||||
wasValidKey = true;
|
||||
Key<ContactResource> contactKey = Key.create(ContactResource.class, pathSearchString);
|
||||
ContactResource contactResource = ofy().load().key(contactKey).now();
|
||||
// As per Andy Newton on the regext mailing list, contacts by themselves have no role, since
|
||||
// they are global, and might have different roles for different domains.
|
||||
if ((contactResource != null) && shouldBeVisible(contactResource)) {
|
||||
if (contactResource != null && isAuthorized(contactResource)) {
|
||||
return rdapJsonFormatter.createRdapContactEntity(
|
||||
contactResource, ImmutableSet.of(), OutputDataType.FULL);
|
||||
}
|
||||
}
|
||||
|
||||
// RDAP Technical Implementation Guide 2.4.1 - MUST support registrar entity lookup using the
|
||||
// IANA ID as handle
|
||||
Long ianaIdentifier = Longs.tryParse(pathSearchString);
|
||||
if (ianaIdentifier != null) {
|
||||
wasValidKey = true;
|
||||
Optional<Registrar> registrar = getRegistrarByIanaIdentifier(ianaIdentifier);
|
||||
if (registrar.isPresent() && shouldBeVisible(registrar.get())) {
|
||||
if (registrar.isPresent() && isAuthorized(registrar.get())) {
|
||||
return rdapJsonFormatter.createRdapRegistrarEntity(registrar.get(), OutputDataType.FULL);
|
||||
}
|
||||
}
|
||||
|
||||
// RDAP Technical Implementation Guide 2.4.2 - MUST support registrar entity lookup using the
|
||||
// fn as handle
|
||||
Optional<Registrar> registrar = getRegistrarByName(pathSearchString);
|
||||
if (registrar.isPresent() && isAuthorized(registrar.get())) {
|
||||
return rdapJsonFormatter.createRdapRegistrarEntity(registrar.get(), OutputDataType.FULL);
|
||||
}
|
||||
|
||||
// At this point, we have failed to find either a contact or a registrar.
|
||||
throw wasValidKey
|
||||
? new NotFoundException(pathSearchString + " not found")
|
||||
: new BadRequestException(pathSearchString + " is not a valid entity handle");
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// Note we don't do RFC7480 5.3 - returning a different code if we wish to say "this info
|
||||
// exists but we don't want to show it to you", because we DON'T wish to say that.
|
||||
throw new NotFoundException(pathSearchString + " not found");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,6 +72,9 @@ import javax.inject.Inject;
|
|||
* (RDAP) Query Format</a>
|
||||
* @see <a href="http://tools.ietf.org/html/rfc7483">RFC 7483: JSON Responses for the Registration
|
||||
* Data Access Protocol (RDAP)</a>
|
||||
*
|
||||
* TODO(guyben):This isn't required by the RDAP Technical Implementation Guide, and hence should be
|
||||
* deleted, at least until it's actually required.
|
||||
*/
|
||||
@Action(
|
||||
service = Action.Service.PUBAPI,
|
||||
|
@ -109,14 +112,9 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
|
|||
|
||||
/** Parses the parameters and calls the appropriate search function. */
|
||||
@Override
|
||||
public EntitySearchResponse getJsonObjectForResource(
|
||||
String pathSearchString, boolean isHeadRequest) {
|
||||
public EntitySearchResponse getSearchResponse(boolean isHeadRequest) {
|
||||
|
||||
// RDAP syntax example: /rdap/entities?fn=Bobby%20Joe*.
|
||||
// The pathSearchString is not used by search commands.
|
||||
if (pathSearchString.length() > 0) {
|
||||
throw new BadRequestException("Unexpected path");
|
||||
}
|
||||
if (Booleans.countTrue(fnParam.isPresent(), handleParam.isPresent()) != 1) {
|
||||
throw new BadRequestException("You must specify either fn=XXXX or handle=YYYY");
|
||||
}
|
||||
|
@ -133,8 +131,6 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
|
|||
throw new BadRequestException("Subtype parameter must specify contacts, registrars or all");
|
||||
}
|
||||
|
||||
// Decode the cursor token and extract the prefix and string portions.
|
||||
decodeCursorToken();
|
||||
CursorType cursorType;
|
||||
Optional<String> cursorQueryString;
|
||||
if (!cursorString.isPresent()) {
|
||||
|
@ -162,7 +158,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
|
|||
// The name is the contact name or registrar name (not registrar contact name).
|
||||
results =
|
||||
searchByName(
|
||||
recordWildcardType(RdapSearchPattern.create(fnParam.get(), false)),
|
||||
recordWildcardType(RdapSearchPattern.createFromUnicodeString(fnParam.get())),
|
||||
cursorType,
|
||||
cursorQueryString,
|
||||
subtype);
|
||||
|
@ -174,7 +170,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
|
|||
// The handle is either the contact roid or the registrar clientId.
|
||||
results =
|
||||
searchByHandle(
|
||||
recordWildcardType(RdapSearchPattern.create(handleParam.get(), false)),
|
||||
recordWildcardType(RdapSearchPattern.createFromUnicodeString(handleParam.get())),
|
||||
cursorType,
|
||||
cursorQueryString,
|
||||
subtype);
|
||||
|
|
|
@ -82,6 +82,8 @@ public class RdapIcannStandardInformation {
|
|||
/**
|
||||
* Required by ICANN RDAP Profile section 1.4.9, as corrected by Gustavo Lozano of ICANN.
|
||||
*
|
||||
* Also mentioned in the RDAP Technical Implementation Guide 3.6.
|
||||
*
|
||||
* @see <a href="http://mm.icann.org/pipermail/gtld-tech/2016-October/000822.html">Questions about
|
||||
* the ICANN RDAP Profile</a>
|
||||
*/
|
||||
|
@ -96,6 +98,8 @@ public class RdapIcannStandardInformation {
|
|||
/**
|
||||
* Required by ICANN RDAP Profile section 1.4.8, as corrected by Gustavo Lozano of ICANN.
|
||||
*
|
||||
* Also mentioned in the RDAP Technical Implementation Guide 3.5.
|
||||
*
|
||||
* @see <a href="http://mm.icann.org/pipermail/gtld-tech/2016-October/000822.html">Questions about
|
||||
* the ICANN RDAP Profile</a>
|
||||
*/
|
||||
|
|
|
@ -298,6 +298,7 @@ public class RdapMetrics {
|
|||
.setSearchType(SearchType.NONE)
|
||||
.setWildcardType(WildcardType.INVALID)
|
||||
.setPrefixLength(0)
|
||||
.setRegistrarSpecified(false)
|
||||
.setIncompletenessWarningType(IncompletenessWarningType.COMPLETE);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,8 @@ public class RdapNameserverAction extends RdapActionBase {
|
|||
|
||||
@Override
|
||||
public RdapNameserver getJsonObjectForResource(String pathSearchString, boolean isHeadRequest) {
|
||||
// RDAP Technical Implementation Guide 2.2.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);
|
||||
// The RDAP syntax is /rdap/nameserver/ns1.mydomain.com.
|
||||
try {
|
||||
|
@ -64,7 +66,12 @@ public class RdapNameserverAction extends RdapActionBase {
|
|||
HostResource.class,
|
||||
pathSearchString,
|
||||
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
|
||||
if (!shouldBeVisible(hostResource)) {
|
||||
if (!hostResource.isPresent() || !isAuthorized(hostResource.get())) {
|
||||
// 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.
|
||||
//
|
||||
// Note we don't do RFC7480 5.3 - returning a different code if we wish to say "this info
|
||||
// exists but we don't want to show it to you", because we DON'T wish to say that.
|
||||
throw new NotFoundException(pathSearchString + " not found");
|
||||
}
|
||||
return rdapJsonFormatter.createRdapNameserver(hostResource.get(), OutputDataType.FULL);
|
||||
|
|
|
@ -36,7 +36,6 @@ import google.registry.request.HttpException.NotFoundException;
|
|||
import google.registry.request.HttpException.UnprocessableEntityException;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Idn;
|
||||
import java.net.InetAddress;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -79,30 +78,26 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
|
|||
* <p>The RDAP spec allows nameserver search by either name or IP address.
|
||||
*/
|
||||
@Override
|
||||
public NameserverSearchResponse getJsonObjectForResource(
|
||||
String pathSearchString, boolean isHeadRequest) {
|
||||
public NameserverSearchResponse getSearchResponse(boolean isHeadRequest) {
|
||||
// RDAP syntax example: /rdap/nameservers?name=ns*.example.com.
|
||||
// The pathSearchString is not used by search commands.
|
||||
if (pathSearchString.length() > 0) {
|
||||
throw new BadRequestException("Unexpected path");
|
||||
}
|
||||
if (Booleans.countTrue(nameParam.isPresent(), ipParam.isPresent()) != 1) {
|
||||
throw new BadRequestException("You must specify either name=XXXX or ip=YYYY");
|
||||
}
|
||||
decodeCursorToken();
|
||||
NameserverSearchResponse results;
|
||||
if (nameParam.isPresent()) {
|
||||
// RDAP Technical Implementation Guilde 2.2.3 - we MAY support nameserver search queries based
|
||||
// on a "nameserver search pattern" as defined in RFC7482
|
||||
//
|
||||
// syntax: /rdap/nameservers?name=exam*.com
|
||||
metricInformationBuilder.setSearchType(SearchType.BY_NAMESERVER_NAME);
|
||||
if (!LDH_PATTERN.matcher(nameParam.get()).matches()) {
|
||||
throw new BadRequestException(
|
||||
"Name parameter must contain only letters, dots"
|
||||
+ " and hyphens, and an optional single wildcard");
|
||||
}
|
||||
results =
|
||||
searchByName(
|
||||
recordWildcardType(RdapSearchPattern.create(Idn.toASCII(nameParam.get()), true)));
|
||||
recordWildcardType(
|
||||
RdapSearchPattern.createFromLdhOrUnicodeDomainName(nameParam.get())));
|
||||
} else {
|
||||
// RDAP Technical Implementation Guide 2.2.3 - we MUST support nameserver search queries based
|
||||
// on IP address as defined in RFC7482 3.2.2. Doesn't require pattern matching
|
||||
//
|
||||
// syntax: /rdap/nameservers?ip=1.2.3.4
|
||||
metricInformationBuilder.setSearchType(SearchType.BY_NAMESERVER_ADDRESS);
|
||||
InetAddress inetAddress;
|
||||
|
@ -130,22 +125,22 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
|
|||
// nameservers are desired, because there may be multiple nameservers with the same name.
|
||||
if (!partialStringQuery.getHasWildcard() && !shouldIncludeDeleted()) {
|
||||
return searchByNameUsingForeignKey(partialStringQuery);
|
||||
}
|
||||
// Handle queries with a wildcard (or including deleted entries). If there is a suffix, it
|
||||
// should be a domain that we manage, so we can look up the domain and search through the
|
||||
// subordinate hosts. This is more efficient, and lets us permit wildcard searches with no
|
||||
// initial string. Deleted nameservers cannot be searched using a suffix, because the logic
|
||||
// of the deletion status of the superordinate domain versus the deletion status of the
|
||||
// subordinate host gets too messy.
|
||||
} else if (partialStringQuery.getSuffix() != null) {
|
||||
if (partialStringQuery.getSuffix() != null) {
|
||||
if (shouldIncludeDeleted()) {
|
||||
throw new UnprocessableEntityException(
|
||||
"A suffix after a wildcard is not allowed when searching for deleted nameservers");
|
||||
}
|
||||
return searchByNameUsingSuperordinateDomain(partialStringQuery);
|
||||
// Handle queries with a wildcard (or deleted entries included), but no suffix.
|
||||
} else {
|
||||
return searchByNameUsingPrefix(partialStringQuery);
|
||||
}
|
||||
// Handle queries with a wildcard (or deleted entries included), but no suffix.
|
||||
return searchByNameUsingPrefix(partialStringQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -155,21 +150,21 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
|
|||
*/
|
||||
private NameserverSearchResponse searchByNameUsingForeignKey(
|
||||
RdapSearchPattern partialStringQuery) {
|
||||
Optional<HostResource> hostResource =
|
||||
loadByForeignKey(
|
||||
HostResource.class, partialStringQuery.getInitialString(), getRequestTime());
|
||||
if (!shouldBeVisible(hostResource)) {
|
||||
metricInformationBuilder.setNumHostsRetrieved(0);
|
||||
throw new NotFoundException("No nameservers found");
|
||||
}
|
||||
metricInformationBuilder.setNumHostsRetrieved(1);
|
||||
|
||||
NameserverSearchResponse.Builder builder =
|
||||
NameserverSearchResponse.builder()
|
||||
.setIncompletenessWarningType(IncompletenessWarningType.COMPLETE);
|
||||
|
||||
Optional<HostResource> hostResource =
|
||||
loadByForeignKey(
|
||||
HostResource.class, partialStringQuery.getInitialString(), getRequestTime());
|
||||
|
||||
metricInformationBuilder.setNumHostsRetrieved(hostResource.isPresent() ? 1 : 0);
|
||||
|
||||
if (shouldBeVisible(hostResource)) {
|
||||
builder
|
||||
.nameserverSearchResultsBuilder()
|
||||
.add(rdapJsonFormatter.createRdapNameserver(hostResource.get(), OutputDataType.FULL));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
@ -215,7 +210,8 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
|
|||
/**
|
||||
* Searches for nameservers by name with a prefix and wildcard.
|
||||
*
|
||||
* <p>There are no pending deletes for hosts, so we can call {@link RdapActionBase#queryItems}.
|
||||
* <p>There are no pending deletes for hosts, so we can call {@link
|
||||
* RdapSearchActionBase#queryItems}.
|
||||
*/
|
||||
private NameserverSearchResponse searchByNameUsingPrefix(RdapSearchPattern partialStringQuery) {
|
||||
// Add 1 so we can detect truncation.
|
||||
|
|
|
@ -163,7 +163,7 @@ final class RdapObjectClasses {
|
|||
*
|
||||
* <p>RFC 7483 specifies that the top-level object should include an entry indicating the
|
||||
* conformance level. ICANN RDAP spec for 15feb19 mandates several additional entries, in sections
|
||||
* 2.6.3, 2.11 of the Response Profile and 3.3.2, 3.5, of the Technical Implementation Guide.
|
||||
* 2.6.3, 2.11 of the Response Profile and 3.3, 3.5, of the Technical Implementation Guide.
|
||||
*/
|
||||
@AutoValue
|
||||
@RestrictJsonNames({})
|
||||
|
|
|
@ -14,17 +14,29 @@
|
|||
|
||||
package google.registry.rdap;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static com.google.common.base.Charsets.UTF_8;
|
||||
import static google.registry.model.ofy.ObjectifyService.ofy;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.googlecode.objectify.Key;
|
||||
import com.googlecode.objectify.cmd.Query;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.rdap.RdapMetrics.EndpointType;
|
||||
import google.registry.rdap.RdapMetrics.WildcardType;
|
||||
import google.registry.rdap.RdapSearchResults.BaseSearchResponse;
|
||||
import google.registry.rdap.RdapSearchResults.IncompletenessWarningType;
|
||||
import google.registry.request.HttpException.BadRequestException;
|
||||
import google.registry.request.HttpException.UnprocessableEntityException;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.ParameterMap;
|
||||
import google.registry.request.RequestUrl;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -39,9 +51,12 @@ import javax.inject.Inject;
|
|||
*/
|
||||
public abstract class RdapSearchActionBase extends RdapActionBase {
|
||||
|
||||
private static final int RESULT_SET_SIZE_SCALING_FACTOR = 30;
|
||||
|
||||
@Inject @RequestUrl String requestUrl;
|
||||
@Inject @ParameterMap ImmutableListMultimap<String, String> parameterMap;
|
||||
@Inject @Parameter("cursor") Optional<String> cursorTokenParam;
|
||||
@Inject @Parameter("registrar") Optional<String> registrarParam;
|
||||
|
||||
protected Optional<String> cursorString;
|
||||
|
||||
|
@ -49,6 +64,20 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
|
|||
super(humanReadableObjectTypeName, endpointType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final BaseSearchResponse getJsonObjectForResource(
|
||||
String pathSearchString, boolean isHeadRequest) {
|
||||
// The pathSearchString is not used by search commands.
|
||||
if (pathSearchString.length() > 0) {
|
||||
throw new BadRequestException("Unexpected path");
|
||||
}
|
||||
decodeCursorToken();
|
||||
metricInformationBuilder.setRegistrarSpecified(registrarParam.isPresent());
|
||||
return getSearchResponse(isHeadRequest);
|
||||
}
|
||||
|
||||
public abstract BaseSearchResponse getSearchResponse(boolean isHeadRequest);
|
||||
|
||||
/**
|
||||
* Decodes the cursor token passed in the HTTP request.
|
||||
*
|
||||
|
@ -57,14 +86,9 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
|
|||
* than the cursor value.
|
||||
*/
|
||||
protected void decodeCursorToken() {
|
||||
if (!cursorTokenParam.isPresent()) {
|
||||
cursorString = Optional.empty();
|
||||
} else {
|
||||
cursorString =
|
||||
Optional.of(
|
||||
new String(
|
||||
Base64.getDecoder().decode(cursorTokenParam.get().getBytes(UTF_8)), UTF_8));
|
||||
}
|
||||
cursorTokenParam.map(
|
||||
cursor -> new String(Base64.getDecoder().decode(cursor.getBytes(UTF_8)), UTF_8));
|
||||
}
|
||||
|
||||
/** Returns an encoded cursor token to pass back in the RDAP JSON link strings. */
|
||||
|
@ -77,6 +101,121 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
|
|||
return getRequestUrlWithExtraParameter(parameterName, ImmutableList.of(parameterValue));
|
||||
}
|
||||
|
||||
/** Returns the registrar on which results should be filtered, or absent(). */
|
||||
protected Optional<String> getDesiredRegistrar() {
|
||||
return registrarParam;
|
||||
}
|
||||
|
||||
protected boolean shouldBeVisible(Optional<? extends EppResource> eppResource) {
|
||||
return eppResource.isPresent() && shouldBeVisible(eppResource.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the EPP resource should be visible.
|
||||
*
|
||||
* <p>This is true iff:
|
||||
* 1. The resource is not deleted, or the request wants to see deleted items, and is authorized to
|
||||
* do so, and:
|
||||
* 2. The request did not specify a registrar to filter on, or the registrar matches.
|
||||
*/
|
||||
protected boolean shouldBeVisible(EppResource eppResource) {
|
||||
return isAuthorized(eppResource)
|
||||
&& (!registrarParam.isPresent()
|
||||
|| registrarParam.get().equals(eppResource.getPersistedCurrentSponsorClientId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the EPP resource should be visible.
|
||||
*
|
||||
* <p>This is true iff:
|
||||
* 1. The resource is not deleted, or the request wants to see deleted items, and is authorized to
|
||||
* do so, and:
|
||||
* 2. The request did not specify a registrar to filter on, or the registrar matches.
|
||||
*/
|
||||
protected boolean shouldBeVisible(Registrar registrar) {
|
||||
return isAuthorized(registrar)
|
||||
&& (!registrarParam.isPresent() || registrarParam.get().equals(registrar.getClientId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the given query, and checks for permissioning if necessary.
|
||||
*
|
||||
* @param query an already-defined query to be run; a filter on currentSponsorClientId will be
|
||||
* added if appropriate
|
||||
* @param checkForVisibility true if the results should be checked to make sure they are visible;
|
||||
* normally this should be equal to the shouldIncludeDeleted setting, but in cases where the
|
||||
* query could not check deletion status (due to Datastore limitations such as the limit of
|
||||
* one field queried for inequality, for instance), it may need to be set to true even when
|
||||
* not including deleted records
|
||||
* @param querySizeLimit the maximum number of items the query is expected to return, usually
|
||||
* because the limit has been set
|
||||
* @return an {@link RdapResultSet} object containing the list of resources and an incompleteness
|
||||
* warning flag, which is set to MIGHT_BE_INCOMPLETE iff any resources were excluded due to
|
||||
* lack of visibility, and the resulting list of resources is less than the maximum allowable,
|
||||
* and the number of items returned by the query is greater than or equal to the maximum
|
||||
* number we might have expected
|
||||
*/
|
||||
<T extends EppResource> RdapResultSet<T> getMatchingResources(
|
||||
Query<T> query, boolean checkForVisibility, int querySizeLimit) {
|
||||
Optional<String> desiredRegistrar = getDesiredRegistrar();
|
||||
if (desiredRegistrar.isPresent()) {
|
||||
query = query.filter("currentSponsorClientId", desiredRegistrar.get());
|
||||
}
|
||||
if (!checkForVisibility) {
|
||||
return RdapResultSet.create(query.list());
|
||||
}
|
||||
// If we are including deleted resources, we need to check that we're authorized for each one.
|
||||
List<T> resources = new ArrayList<>();
|
||||
int numResourcesQueried = 0;
|
||||
boolean someExcluded = false;
|
||||
for (T resource : query) {
|
||||
if (shouldBeVisible(resource)) {
|
||||
resources.add(resource);
|
||||
} else {
|
||||
someExcluded = true;
|
||||
}
|
||||
numResourcesQueried++;
|
||||
if (resources.size() > rdapResultSetMaxSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// The incompleteness problem comes about because we don't know how many items to fetch. We want
|
||||
// to return rdapResultSetMaxSize worth of items, but some might be excluded, so we fetch more
|
||||
// just in case. But how many more? That's the potential problem, addressed with the three way
|
||||
// AND statement:
|
||||
// 1. If we didn't exclude any items, then we can't have the incompleteness problem.
|
||||
// 2. If have a full result set batch (rdapResultSetMaxSize items), we must by definition be
|
||||
// giving the user a complete result set.
|
||||
// 3. If we started with fewer than querySizeLimit items, then there weren't any more items that
|
||||
// we missed. Even if we return fewer than rdapResultSetMaxSize items, it isn't because we
|
||||
// didn't fetch enough to start.
|
||||
// Only if all three conditions are true might things be incomplete. In other words, we fetched
|
||||
// as many as our limit allowed, but then excluded so many that we wound up with less than a
|
||||
// full result set's worth of results.
|
||||
return RdapResultSet.create(
|
||||
resources,
|
||||
(someExcluded
|
||||
&& (resources.size() < rdapResultSetMaxSize)
|
||||
&& (numResourcesQueried >= querySizeLimit))
|
||||
? IncompletenessWarningType.MIGHT_BE_INCOMPLETE
|
||||
: IncompletenessWarningType.COMPLETE,
|
||||
numResourcesQueried);
|
||||
}
|
||||
|
||||
RdapSearchPattern recordWildcardType(RdapSearchPattern partialStringQuery) {
|
||||
if (!partialStringQuery.getHasWildcard()) {
|
||||
metricInformationBuilder.setWildcardType(WildcardType.NO_WILDCARD);
|
||||
} else if (partialStringQuery.getSuffix() == null) {
|
||||
metricInformationBuilder.setWildcardType(WildcardType.PREFIX);
|
||||
} else if (partialStringQuery.getInitialString().isEmpty()) {
|
||||
metricInformationBuilder.setWildcardType(WildcardType.SUFFIX);
|
||||
} else {
|
||||
metricInformationBuilder.setWildcardType(WildcardType.PREFIX_AND_SUFFIX);
|
||||
}
|
||||
metricInformationBuilder.setPrefixLength(partialStringQuery.getInitialString().length());
|
||||
return partialStringQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the original request URL, but with the specified parameter added or overridden.
|
||||
*
|
||||
|
@ -123,4 +262,158 @@ public abstract class RdapSearchActionBase extends RdapActionBase {
|
|||
URI createNavigationUri(String cursor) {
|
||||
return URI.create(getRequestUrlWithExtraParameter("cursor", encodeCursorToken(cursor)));
|
||||
}
|
||||
|
||||
// We want to return rdapResultSetMaxSize + 1 results, so that we know if there are "extra"
|
||||
// results (in which case we'll have a "next" link in the RDAP response).
|
||||
// In case that we want to return deleted results as well, we have to scale the number of results
|
||||
// to be (more) sure we got everything.
|
||||
int getStandardQuerySizeLimit() {
|
||||
return shouldIncludeDeleted()
|
||||
? (RESULT_SET_SIZE_SCALING_FACTOR * (rdapResultSetMaxSize + 1))
|
||||
: (rdapResultSetMaxSize + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles prefix searches in cases where, if we need to filter out deleted items, there are no
|
||||
* pending deletes.
|
||||
*
|
||||
* <p>In such cases, it is sufficient to check whether {@code deletionTime} is equal to
|
||||
* {@code END_OF_TIME}, because any other value means it has already been deleted. This allows us
|
||||
* to use an equality query for the deletion time.
|
||||
*
|
||||
* @param clazz the type of resource to be queried
|
||||
* @param filterField the database field of interest
|
||||
* @param partialStringQuery the details of the search string; if there is no wildcard, an
|
||||
* equality query is used; if there is a wildcard, a range query is used instead; the
|
||||
* initial string should not be empty, and any search suffix will be ignored, so the caller
|
||||
* must filter the results if a suffix is specified
|
||||
* @param cursorString if a cursor is present, this parameter should specify the cursor string, to
|
||||
* skip any results up to and including the string; empty() if there is no cursor
|
||||
* @param deletedItemHandling whether to include or exclude deleted items
|
||||
* @param resultSetMaxSize the maximum number of results to return
|
||||
* @return the query object
|
||||
*/
|
||||
static <T extends EppResource> Query<T> queryItems(
|
||||
Class<T> clazz,
|
||||
String filterField,
|
||||
RdapSearchPattern partialStringQuery,
|
||||
Optional<String> cursorString,
|
||||
DeletedItemHandling deletedItemHandling,
|
||||
int resultSetMaxSize) {
|
||||
if (partialStringQuery.getInitialString().length()
|
||||
< RdapSearchPattern.MIN_INITIAL_STRING_LENGTH) {
|
||||
throw new UnprocessableEntityException(
|
||||
String.format(
|
||||
"Initial search string must be at least %d characters",
|
||||
RdapSearchPattern.MIN_INITIAL_STRING_LENGTH));
|
||||
}
|
||||
Query<T> query = ofy().load().type(clazz);
|
||||
if (!partialStringQuery.getHasWildcard()) {
|
||||
query = query.filter(filterField, partialStringQuery.getInitialString());
|
||||
} else {
|
||||
// Ignore the suffix; the caller will need to filter on the suffix, if any.
|
||||
query = query
|
||||
.filter(filterField + " >=", partialStringQuery.getInitialString())
|
||||
.filter(filterField + " <", partialStringQuery.getNextInitialString());
|
||||
}
|
||||
if (cursorString.isPresent()) {
|
||||
query = query.filter(filterField + " >", cursorString.get());
|
||||
}
|
||||
return setOtherQueryAttributes(query, deletedItemHandling, resultSetMaxSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles searches using a simple string rather than an {@link RdapSearchPattern}.
|
||||
*
|
||||
* <p>Since the filter is not an inequality, we can support also checking a cursor string against
|
||||
* a different field (which involves an inequality on that field).
|
||||
*
|
||||
* @param clazz the type of resource to be queried
|
||||
* @param filterField the database field of interest
|
||||
* @param queryString the search string
|
||||
* @param cursorField the field which should be compared to the cursor string, or empty() if the
|
||||
* key should be compared to a key created from the cursor string
|
||||
* @param cursorString if a cursor is present, this parameter should specify the cursor string, to
|
||||
* skip any results up to and including the string; empty() if there is no cursor
|
||||
* @param deletedItemHandling whether to include or exclude deleted items
|
||||
* @param resultSetMaxSize the maximum number of results to return
|
||||
* @return the query object
|
||||
*/
|
||||
static <T extends EppResource> Query<T> queryItems(
|
||||
Class<T> clazz,
|
||||
String filterField,
|
||||
String queryString,
|
||||
Optional<String> cursorField,
|
||||
Optional<String> cursorString,
|
||||
DeletedItemHandling deletedItemHandling,
|
||||
int resultSetMaxSize) {
|
||||
if (queryString.length() < RdapSearchPattern.MIN_INITIAL_STRING_LENGTH) {
|
||||
throw new UnprocessableEntityException(
|
||||
String.format(
|
||||
"Initial search string must be at least %d characters",
|
||||
RdapSearchPattern.MIN_INITIAL_STRING_LENGTH));
|
||||
}
|
||||
Query<T> query = ofy().load().type(clazz).filter(filterField, queryString);
|
||||
if (cursorString.isPresent()) {
|
||||
if (cursorField.isPresent()) {
|
||||
query = query.filter(cursorField.get() + " >", cursorString.get());
|
||||
} else {
|
||||
query = query.filterKey(">", Key.create(clazz, cursorString.get()));
|
||||
}
|
||||
}
|
||||
return setOtherQueryAttributes(query, deletedItemHandling, resultSetMaxSize);
|
||||
}
|
||||
|
||||
/** Handles searches where the field to be searched is the key. */
|
||||
static <T extends EppResource> Query<T> queryItemsByKey(
|
||||
Class<T> clazz,
|
||||
RdapSearchPattern partialStringQuery,
|
||||
Optional<String> cursorString,
|
||||
DeletedItemHandling deletedItemHandling,
|
||||
int resultSetMaxSize) {
|
||||
if (partialStringQuery.getInitialString().length()
|
||||
< RdapSearchPattern.MIN_INITIAL_STRING_LENGTH) {
|
||||
throw new UnprocessableEntityException(
|
||||
String.format(
|
||||
"Initial search string must be at least %d characters",
|
||||
RdapSearchPattern.MIN_INITIAL_STRING_LENGTH));
|
||||
}
|
||||
Query<T> query = ofy().load().type(clazz);
|
||||
if (!partialStringQuery.getHasWildcard()) {
|
||||
query = query.filterKey("=", Key.create(clazz, partialStringQuery.getInitialString()));
|
||||
} else {
|
||||
// Ignore the suffix; the caller will need to filter on the suffix, if any.
|
||||
query = query
|
||||
.filterKey(">=", Key.create(clazz, partialStringQuery.getInitialString()))
|
||||
.filterKey("<", Key.create(clazz, partialStringQuery.getNextInitialString()));
|
||||
}
|
||||
if (cursorString.isPresent()) {
|
||||
query = query.filterKey(">", Key.create(clazz, cursorString.get()));
|
||||
}
|
||||
return setOtherQueryAttributes(query, deletedItemHandling, resultSetMaxSize);
|
||||
}
|
||||
|
||||
/** Handles searches by key using a simple string. */
|
||||
static <T extends EppResource> Query<T> queryItemsByKey(
|
||||
Class<T> clazz,
|
||||
String queryString,
|
||||
DeletedItemHandling deletedItemHandling,
|
||||
int resultSetMaxSize) {
|
||||
if (queryString.length() < RdapSearchPattern.MIN_INITIAL_STRING_LENGTH) {
|
||||
throw new UnprocessableEntityException(
|
||||
String.format(
|
||||
"Initial search string must be at least %d characters",
|
||||
RdapSearchPattern.MIN_INITIAL_STRING_LENGTH));
|
||||
}
|
||||
Query<T> query = ofy().load().type(clazz).filterKey("=", Key.create(clazz, queryString));
|
||||
return setOtherQueryAttributes(query, deletedItemHandling, resultSetMaxSize);
|
||||
}
|
||||
|
||||
static <T extends EppResource> Query<T> setOtherQueryAttributes(
|
||||
Query<T> query, DeletedItemHandling deletedItemHandling, int resultSetMaxSize) {
|
||||
if (deletedItemHandling != DeletedItemHandling.INCLUDE) {
|
||||
query = query.filter("deletionTime", END_OF_TIME);
|
||||
}
|
||||
return query.limit(resultSetMaxSize);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,9 +14,13 @@
|
|||
|
||||
package google.registry.rdap;
|
||||
|
||||
import static google.registry.util.DomainNameUtils.ACE_PREFIX;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import google.registry.request.HttpException.BadRequestException;
|
||||
import google.registry.request.HttpException.UnprocessableEntityException;
|
||||
import google.registry.util.Idn;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
|
@ -32,6 +36,19 @@ public final class RdapSearchPattern {
|
|||
|
||||
static final int MIN_INITIAL_STRING_LENGTH = 2;
|
||||
|
||||
/**
|
||||
* Pattern for allowed LDH searches.
|
||||
*
|
||||
* <p>Based on RFC7482 4.1. Must contains only alphanumeric plus dots and hyphens. A single
|
||||
* whildcard asterix is allowed - but if exists must be the last character of a domain name label
|
||||
* (so exam* and exam*.com are allowed, but exam*le.com isn't allowd)
|
||||
*
|
||||
* <p>The prefix is in group(1), and the suffix without the dot (if it exists) is in group(4). If
|
||||
* there's no wildcard, group(2) is empty.
|
||||
*/
|
||||
static final Pattern LDH_PATTERN =
|
||||
Pattern.compile("([-.a-zA-Z0-9]*)([*]([.]([-.a-zA-Z0-9]+))?)?");
|
||||
|
||||
/** String before the wildcard character. */
|
||||
private final String initialString;
|
||||
|
||||
|
@ -78,51 +95,83 @@ public final class RdapSearchPattern {
|
|||
}
|
||||
|
||||
/**
|
||||
* Creates a SearchPattern using the provided search pattern string.
|
||||
* Creates a SearchPattern using the provided search pattern string in Unicode.
|
||||
*
|
||||
* @param pattern the string containing the partial match pattern
|
||||
* @param allowSuffix true if a suffix is allowed after the wildcard
|
||||
* <p>The search query might end in an asterix, in which case that asterix is considered a
|
||||
* wildcard and can match 0 or more characters. Without that asterix - the match will be exact.
|
||||
*
|
||||
* @param searchQuery the string containing the partial match pattern, optionally ending in a
|
||||
* wildcard asterix
|
||||
* @throws UnprocessableEntityException if {@code pattern} has a wildcard not at the end of the
|
||||
* query
|
||||
*/
|
||||
public static RdapSearchPattern createFromUnicodeString(String searchQuery) {
|
||||
int wildcardLocation = searchQuery.indexOf('*');
|
||||
if (wildcardLocation < 0) {
|
||||
return new RdapSearchPattern(searchQuery, false, null);
|
||||
}
|
||||
if (wildcardLocation == searchQuery.length() - 1) {
|
||||
return new RdapSearchPattern(searchQuery.substring(0, wildcardLocation), true, null);
|
||||
}
|
||||
throw new UnprocessableEntityException(
|
||||
String.format(
|
||||
"Query can only have a single wildcard, and it must be at the end of the query, but"
|
||||
+ " was: '%s'",
|
||||
searchQuery));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a SearchPattern using the provided domain search pattern in LDH format.
|
||||
*
|
||||
* <p>The domain search pattern can have a single wildcard asterix that can match 0 or more
|
||||
* charecters. If such an asterix exists - it must be at the end of a domain label.
|
||||
*
|
||||
* @param searchQuery the string containing the partial match pattern
|
||||
* @throws UnprocessableEntityException if {@code pattern} does not meet the requirements of RFC
|
||||
* 7482
|
||||
*/
|
||||
public static RdapSearchPattern create(String pattern, boolean allowSuffix) {
|
||||
String initialString;
|
||||
boolean hasWildcard;
|
||||
String suffix;
|
||||
// If there's no wildcard character, just lump everything into the initial string.
|
||||
int wildcardPos = pattern.indexOf('*');
|
||||
if (wildcardPos < 0) {
|
||||
initialString = pattern;
|
||||
hasWildcard = false;
|
||||
suffix = null;
|
||||
} else if (pattern.indexOf('*', wildcardPos + 1) >= 0) {
|
||||
throw new UnprocessableEntityException("Only one wildcard allowed");
|
||||
} else {
|
||||
hasWildcard = true;
|
||||
// Check for a suffix (e.g. exam*.com or ns*.example.com).
|
||||
if (pattern.length() > wildcardPos + 1) {
|
||||
if (!allowSuffix) {
|
||||
throw new UnprocessableEntityException("Suffix not allowed after wildcard");
|
||||
}
|
||||
if ((pattern.length() == wildcardPos + 2) || (pattern.charAt(wildcardPos + 1) != '.')) {
|
||||
public static RdapSearchPattern createFromLdhDomainName(String searchQuery) {
|
||||
Matcher matcher = LDH_PATTERN.matcher(searchQuery);
|
||||
if (!matcher.matches()) {
|
||||
throw new UnprocessableEntityException(
|
||||
"Suffix after wildcard must be one or more domain"
|
||||
+ " name labels, e.g. exam*.tld, ns*.example.tld");
|
||||
}
|
||||
suffix = pattern.substring(wildcardPos + 2);
|
||||
} else {
|
||||
suffix = null;
|
||||
}
|
||||
initialString = pattern.substring(0, wildcardPos);
|
||||
if (initialString.startsWith(ACE_PREFIX) && (initialString.length() < 7)) {
|
||||
throw new UnprocessableEntityException(
|
||||
"At least seven characters must be specified for punycode domain searches");
|
||||
}
|
||||
String.format(
|
||||
"Query can only have a single wildcard, and it must be at the end of a label,"
|
||||
+ " but was: '%s'",
|
||||
searchQuery));
|
||||
}
|
||||
|
||||
String initialString = matcher.group(1);
|
||||
boolean hasWildcard = !Strings.isNullOrEmpty(matcher.group(2));
|
||||
String suffix = Strings.emptyToNull(matcher.group(4));
|
||||
|
||||
return new RdapSearchPattern(initialString, hasWildcard, suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a SearchPattern using the provided domain search pattern in LDH or Unicode format.
|
||||
*
|
||||
* <p>The domain search pattern can have a single wildcard asterix that can match 0 or more
|
||||
* charecters. If such an asterix exists - it must be at the end of a domain label.
|
||||
*
|
||||
* <p>In theory, according to RFC7482 4.1 - we should make some checks about partial matching in
|
||||
* unicode queries. We don't, but we might want to just disable partial matches for unicode inputs
|
||||
* (meaning if it doesn't match LDH_PATTERN, then don't allow wildcard at all).
|
||||
*
|
||||
* @param searchQuery the string containing the partial match pattern
|
||||
* @throws UnprocessableEntityException if {@code pattern} does not meet the requirements of RFC
|
||||
* 7482
|
||||
*/
|
||||
public static RdapSearchPattern createFromLdhOrUnicodeDomainName(String searchQuery) {
|
||||
String ldhSearchQuery;
|
||||
try {
|
||||
ldhSearchQuery = Idn.toASCII(searchQuery);
|
||||
} catch (Exception e) {
|
||||
throw new BadRequestException(
|
||||
String.format("Invalid value of searchQuery: '%s'", searchQuery), e);
|
||||
}
|
||||
return RdapSearchPattern.createFromLdhDomainName(ldhSearchQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a string to make sure that it matches the search pattern.
|
||||
*
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
package google.registry.rdap;
|
||||
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.collect.Streams;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import java.util.Objects;
|
||||
|
@ -30,4 +31,17 @@ public final class RdapUtils {
|
|||
.filter(registrar -> Objects.equals(ianaIdentifier, registrar.getIanaIdentifier()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a registrar by its name.
|
||||
*
|
||||
* <p>Used for RDAP Technical Implementation Guide 2.4.2 - search of registrar by the fn element.
|
||||
*
|
||||
* <p>For convenience, we use case insensitive search.
|
||||
*/
|
||||
static Optional<Registrar> getRegistrarByName(String registrarName) {
|
||||
return Streams.stream(Registrar.loadAllCached())
|
||||
.filter(registrar -> Ascii.equalsIgnoreCase(registrarName, registrar.getRegistrarName()))
|
||||
.findFirst();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,7 +86,6 @@ public class RdapActionBaseTestCase<A extends RdapActionBase> {
|
|||
inject.setStaticField(Ofy.class, "clock", clock);
|
||||
action = TypeUtils.instantiate(rdapActionClass);
|
||||
action.includeDeletedParam = Optional.empty();
|
||||
action.registrarParam = Optional.empty();
|
||||
action.formatOutputParam = Optional.empty();
|
||||
action.response = response;
|
||||
action.rdapJsonFormatter = RdapTestHelper.getTestRdapJsonFormatter(clock);
|
||||
|
|
|
@ -45,7 +45,12 @@ public final class RdapDataStructuresTest {
|
|||
@Test
|
||||
public void testRdapConformance() {
|
||||
assertThat(RdapConformance.INSTANCE.toJson())
|
||||
.isEqualTo(createJson("['rdap_level_0','icann_rdap_response_profile_0']"));
|
||||
.isEqualTo(createJson(
|
||||
"[",
|
||||
" 'rdap_level_0',",
|
||||
" 'icann_rdap_response_profile_0',",
|
||||
" 'icann_rdap_technical_implementation_guide_0'",
|
||||
"]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -273,20 +273,6 @@ public class RdapDomainActionTest extends RdapActionBaseTestCase<RdapDomainActio
|
|||
assertProperResponseForCatLol("cat.lol", "rdap_domain.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidDomain_works_sameRegistrarRequested() {
|
||||
action.registrarParam = Optional.of("evilregistrar");
|
||||
login("evilregistrar");
|
||||
assertProperResponseForCatLol("cat.lol", "rdap_domain.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidDomain_notFound_differentRegistrarRequested() {
|
||||
action.registrarParam = Optional.of("idnregistrar");
|
||||
generateActualJson("cat.lol");
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidDomain_asAdministrator_works() {
|
||||
loginAsAdmin();
|
||||
|
|
|
@ -663,9 +663,10 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDom
|
|||
@Test
|
||||
public void testInvalidWildcard_rejected() {
|
||||
assertThat(generateActualJson(RequestType.NAME, "exam*ple"))
|
||||
.isEqualTo(generateExpectedJsonError(
|
||||
"Suffix after wildcard must be one or more domain"
|
||||
+ " name labels, e.g. exam*.tld, ns*.example.tld",
|
||||
.isEqualTo(
|
||||
generateExpectedJsonError(
|
||||
"Query can only have a single wildcard, and it must be at the end of a label, but"
|
||||
+ " was: 'exam*ple'",
|
||||
422));
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
verifyErrorMetrics(SearchType.BY_DOMAIN_NAME, Optional.empty(), 422);
|
||||
|
@ -674,7 +675,11 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDom
|
|||
@Test
|
||||
public void testMultipleWildcards_rejected() {
|
||||
assertThat(generateActualJson(RequestType.NAME, "*.*"))
|
||||
.isEqualTo(generateExpectedJsonError("Only one wildcard allowed", 422));
|
||||
.isEqualTo(
|
||||
generateExpectedJsonError(
|
||||
"Query can only have a single wildcard, and it must be at the end of a label, but"
|
||||
+ " was: '*.*'",
|
||||
422));
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
verifyErrorMetrics(SearchType.BY_DOMAIN_NAME, Optional.empty(), 422);
|
||||
}
|
||||
|
@ -1297,7 +1302,7 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDom
|
|||
|
||||
@Test
|
||||
public void testNameserverMatchWithWildcardAndEmptySuffix_unprocessable() {
|
||||
rememberWildcardType("ns*.");
|
||||
rememberWildcardTypeInvalid();
|
||||
generateActualJson(RequestType.NS_LDH_NAME, "ns*.");
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
verifyErrorMetrics(SearchType.BY_NAMESERVER_NAME, Optional.empty(), 422);
|
||||
|
@ -1331,8 +1336,8 @@ public class RdapDomainSearchActionTest extends RdapSearchActionTestCase<RdapDom
|
|||
metricWildcardType = WildcardType.INVALID;
|
||||
metricPrefixLength = 0;
|
||||
generateActualJson(RequestType.NS_LDH_NAME, "ns1.cat.みんな");
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
verifyErrorMetrics(SearchType.BY_NAMESERVER_NAME, Optional.empty(), 400);
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
verifyErrorMetrics(SearchType.BY_NAMESERVER_NAME, Optional.empty(), 422);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -163,76 +163,64 @@ public class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityActio
|
|||
return obj;
|
||||
}
|
||||
|
||||
private void runSuccessfulTest(String queryString, String fileName) {
|
||||
runSuccessfulTest(queryString, "(◕‿◕)", "active", null, fileName);
|
||||
private void runSuccessfulHandleTest(String handleQuery, String fileName) {
|
||||
runSuccessfulHandleTest(handleQuery, "(◕‿◕)", "active", null, fileName);
|
||||
}
|
||||
|
||||
private void runSuccessfulTest(String queryString, String fullName, String fileName) {
|
||||
runSuccessfulTest(queryString, fullName, "active", null, fileName);
|
||||
private void runSuccessfulHandleTest(String handleQuery, String fullName, String fileName) {
|
||||
runSuccessfulHandleTest(handleQuery, fullName, "active", null, fileName);
|
||||
}
|
||||
|
||||
private void runSuccessfulTest(
|
||||
String queryString,
|
||||
private void runSuccessfulHandleTest(
|
||||
String handleQuery,
|
||||
String fullName,
|
||||
String rdapStatus,
|
||||
String address,
|
||||
String fileName) {
|
||||
assertThat(generateActualJson(queryString))
|
||||
assertThat(generateActualJson(handleQuery))
|
||||
.isEqualTo(
|
||||
generateExpectedJsonWithTopLevelEntries(
|
||||
queryString, fullName, rdapStatus, address, fileName));
|
||||
handleQuery, fullName, rdapStatus, address, fileName));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
private void runNotFoundTest(String queryString) {
|
||||
assertThat(generateActualJson(queryString))
|
||||
.isEqualTo(generateExpectedJsonError(queryString + " not found", 404));
|
||||
private void runNotFoundTest(String handleQuery) {
|
||||
assertThat(generateActualJson(handleQuery))
|
||||
.isEqualTo(generateExpectedJsonError(handleQuery + " not found", 404));
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidEntity_returns400() {
|
||||
assertThat(generateActualJson("invalid/entity/handle")).isEqualTo(
|
||||
generateExpectedJsonError(
|
||||
"invalid/entity/handle is not a valid entity handle",
|
||||
400));
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
public void testUnknownEntity_RoidPattern_notFound() {
|
||||
runNotFoundTest("_MISSING-ENTITY_");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnknownEntity_notFound() {
|
||||
runNotFoundTest("_MISSING-ENTITY_");
|
||||
public void testUnknownEntity_IanaPattern_notFound() {
|
||||
runNotFoundTest("123");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnknownEntity_notRoidNotIana_notFound() {
|
||||
// Since we allow search by registrar name, every string is a possible name
|
||||
runNotFoundTest("some,random,string");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidRegistrantContact_works() {
|
||||
login("evilregistrar");
|
||||
runSuccessfulTest(registrant.getRepoId(), "rdap_associated_contact.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidRegistrantContact_found_sameRegistrarRequested() {
|
||||
login("evilregistrar");
|
||||
action.registrarParam = Optional.of("evilregistrar");
|
||||
runSuccessfulTest(registrant.getRepoId(), "rdap_associated_contact.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidRegistrantContact_notFound_differentRegistrarRequested() {
|
||||
login("evilregistrar");
|
||||
action.registrarParam = Optional.of("idnregistrar");
|
||||
runNotFoundTest(registrant.getRepoId());
|
||||
runSuccessfulHandleTest(registrant.getRepoId(), "rdap_associated_contact.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidRegistrantContact_found_asAdministrator() {
|
||||
loginAsAdmin();
|
||||
runSuccessfulTest(registrant.getRepoId(), "rdap_associated_contact.json");
|
||||
runSuccessfulHandleTest(registrant.getRepoId(), "rdap_associated_contact.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidRegistrantContact_found_notLoggedIn() {
|
||||
runSuccessfulTest(
|
||||
runSuccessfulHandleTest(
|
||||
registrant.getRepoId(),
|
||||
"(◕‿◕)",
|
||||
"active",
|
||||
|
@ -243,7 +231,7 @@ public class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityActio
|
|||
@Test
|
||||
public void testValidRegistrantContact_found_loggedInAsOtherRegistrar() {
|
||||
login("otherregistrar");
|
||||
runSuccessfulTest(
|
||||
runSuccessfulHandleTest(
|
||||
registrant.getRepoId(),
|
||||
"(◕‿◕)",
|
||||
"active",
|
||||
|
@ -254,19 +242,19 @@ public class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityActio
|
|||
@Test
|
||||
public void testValidAdminContact_works() {
|
||||
login("evilregistrar");
|
||||
runSuccessfulTest(adminContact.getRepoId(), "rdap_associated_contact.json");
|
||||
runSuccessfulHandleTest(adminContact.getRepoId(), "rdap_associated_contact.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidTechContact_works() {
|
||||
login("evilregistrar");
|
||||
runSuccessfulTest(techContact.getRepoId(), "rdap_associated_contact.json");
|
||||
runSuccessfulHandleTest(techContact.getRepoId(), "rdap_associated_contact.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidDisconnectedContact_works() {
|
||||
login("evilregistrar");
|
||||
runSuccessfulTest(disconnectedContact.getRepoId(), "rdap_contact.json");
|
||||
runSuccessfulHandleTest(disconnectedContact.getRepoId(), "rdap_contact.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -297,7 +285,7 @@ public class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityActio
|
|||
public void testDeletedContact_found_loggedInAsCorrectRegistrar() {
|
||||
login("evilregistrar");
|
||||
action.includeDeletedParam = Optional.of(true);
|
||||
runSuccessfulTest(
|
||||
runSuccessfulHandleTest(
|
||||
deletedContact.getRepoId(),
|
||||
"",
|
||||
"inactive",
|
||||
|
@ -309,7 +297,7 @@ public class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityActio
|
|||
public void testDeletedContact_found_loggedInAsAdmin() {
|
||||
loginAsAdmin();
|
||||
action.includeDeletedParam = Optional.of(true);
|
||||
runSuccessfulTest(
|
||||
runSuccessfulHandleTest(
|
||||
deletedContact.getRepoId(),
|
||||
"",
|
||||
"inactive",
|
||||
|
@ -319,29 +307,26 @@ public class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityActio
|
|||
|
||||
@Test
|
||||
public void testRegistrar_found() {
|
||||
runSuccessfulTest("101", "Yes Virginia <script>", "rdap_registrar.json");
|
||||
runSuccessfulHandleTest("101", "Yes Virginia <script>", "rdap_registrar.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegistrarByName_found() {
|
||||
assertThat(generateActualJson("IDN%20Registrar"))
|
||||
.isEqualTo(
|
||||
generateExpectedJsonWithTopLevelEntries(
|
||||
"102", "IDN Registrar", "active", null, "rdap_registrar.json"));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegistrar102_works() {
|
||||
runSuccessfulTest("102", "IDN Registrar", "rdap_registrar.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegistrar102_found_requestingSameRegistrar() {
|
||||
action.registrarParam = Optional.of("idnregistrar");
|
||||
runSuccessfulTest("102", "IDN Registrar", "rdap_registrar.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegistrar102_notFound_requestingOtherRegistrar() {
|
||||
action.registrarParam = Optional.of("1tldregistrar");
|
||||
runNotFoundTest("102");
|
||||
runSuccessfulHandleTest("102", "IDN Registrar", "rdap_registrar.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRegistrar103_works() {
|
||||
runSuccessfulTest("103", "Multilevel Registrar", "rdap_registrar.json");
|
||||
runSuccessfulHandleTest("103", "Multilevel Registrar", "rdap_registrar.json");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -359,7 +344,7 @@ public class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityActio
|
|||
public void testRegistrar104_found_deletedFlagWhenLoggedIn() {
|
||||
login("deletedregistrar");
|
||||
action.includeDeletedParam = Optional.of(true);
|
||||
runSuccessfulTest(
|
||||
runSuccessfulHandleTest(
|
||||
"104", "Yes Virginia <script>", "inactive", null, "rdap_registrar.json");
|
||||
}
|
||||
|
||||
|
@ -374,7 +359,7 @@ public class RdapEntityActionTest extends RdapActionBaseTestCase<RdapEntityActio
|
|||
public void testRegistrar104_found_deletedFlagWhenLoggedInAsAdmin() {
|
||||
loginAsAdmin();
|
||||
action.includeDeletedParam = Optional.of(true);
|
||||
runSuccessfulTest(
|
||||
runSuccessfulHandleTest(
|
||||
"104", "Yes Virginia <script>", "inactive", null, "rdap_registrar.json");
|
||||
}
|
||||
|
||||
|
|
|
@ -412,7 +412,10 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEnt
|
|||
public void testNameMatch_suffixRejected() {
|
||||
assertThat(generateActualJsonWithFullName("exam*ple"))
|
||||
.isEqualTo(
|
||||
generateExpectedJsonError("Suffix not allowed after wildcard", 422));
|
||||
generateExpectedJsonError(
|
||||
"Query can only have a single wildcard, and it must be at the end of the query,"
|
||||
+ " but was: 'exam*ple'",
|
||||
422));
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
verifyErrorMetrics(Optional.empty(), 422);
|
||||
}
|
||||
|
@ -421,7 +424,10 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEnt
|
|||
public void testHandleMatch_suffixRejected() {
|
||||
assertThat(generateActualJsonWithHandle("exam*ple"))
|
||||
.isEqualTo(
|
||||
generateExpectedJsonError("Suffix not allowed after wildcard", 422));
|
||||
generateExpectedJsonError(
|
||||
"Query can only have a single wildcard, and it must be at the end of the query,"
|
||||
+ " but was: 'exam*ple'",
|
||||
422));
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
verifyErrorMetrics(Optional.empty(), 422);
|
||||
}
|
||||
|
@ -429,7 +435,11 @@ public class RdapEntitySearchActionTest extends RdapSearchActionTestCase<RdapEnt
|
|||
@Test
|
||||
public void testMultipleWildcards_rejected() {
|
||||
assertThat(generateActualJsonWithHandle("*.*"))
|
||||
.isEqualTo(generateExpectedJsonError("Only one wildcard allowed", 422));
|
||||
.isEqualTo(
|
||||
generateExpectedJsonError(
|
||||
"Query can only have a single wildcard, and it must be at the end of the query,"
|
||||
+ " but was: '*.*'",
|
||||
422));
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
verifyErrorMetrics(Optional.empty(), 422);
|
||||
}
|
||||
|
|
|
@ -235,30 +235,6 @@ public class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameser
|
|||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNameserver_found_sameRegistrarRequested() {
|
||||
action.registrarParam = Optional.of("TheRegistrar");
|
||||
assertThat(generateActualJson("ns1.cat.lol"))
|
||||
.isEqualTo(
|
||||
generateExpectedJsonWithTopLevelEntries(
|
||||
"ns1.cat.lol",
|
||||
ImmutableMap.of(
|
||||
"HANDLE", "2-ROID",
|
||||
"ADDRESSTYPE", "v4",
|
||||
"ADDRESS", "1.2.3.4",
|
||||
"STATUS", "active"),
|
||||
"rdap_host.json"));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNameserver_notFound_differentRegistrarRequested() {
|
||||
action.registrarParam = Optional.of("otherregistrar");
|
||||
action.includeDeletedParam = Optional.of(false);
|
||||
generateActualJson("ns1.cat.lol");
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeletedNameserver_notFound_includeDeletedNotSpecified() {
|
||||
generateActualJson("nsdeleted.cat.lol");
|
||||
|
@ -322,33 +298,6 @@ public class RdapNameserverActionTest extends RdapActionBaseTestCase<RdapNameser
|
|||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeletedNameserver_found_sameRegistrarRequested() {
|
||||
login("TheRegistrar");
|
||||
action.registrarParam = Optional.of("TheRegistrar");
|
||||
action.includeDeletedParam = Optional.of(true);
|
||||
assertThat(generateActualJson("nsdeleted.cat.lol"))
|
||||
.isEqualTo(
|
||||
generateExpectedJsonWithTopLevelEntries(
|
||||
"nsdeleted.cat.lol",
|
||||
ImmutableMap.of(
|
||||
"HANDLE", "A-ROID",
|
||||
"ADDRESSTYPE", "v4",
|
||||
"ADDRESS", "1.2.3.4",
|
||||
"STATUS", "inactive"),
|
||||
"rdap_host.json"));
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeletedNameserver_notFound_differentRegistrarRequested() {
|
||||
login("TheRegistrar");
|
||||
action.registrarParam = Optional.of("otherregistrar");
|
||||
action.includeDeletedParam = Optional.of(false);
|
||||
generateActualJson("ns1.cat.lol");
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMetrics() {
|
||||
generateActualJson("ns1.cat.lol");
|
||||
|
|
|
@ -258,9 +258,10 @@ public class RdapNameserverSearchActionTest
|
|||
assertThat(generateActualJsonWithName("exam*ple"))
|
||||
.isEqualTo(
|
||||
generateExpectedJsonError(
|
||||
"Suffix after wildcard must be one or more domain"
|
||||
+ " name labels, e.g. exam*.tld, ns*.example.tld",
|
||||
"Query can only have a single wildcard, and it must be at the end of a label, but"
|
||||
+ " was: 'exam*ple'",
|
||||
422));
|
||||
rememberWildcardTypeInvalid();
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
verifyErrorMetrics(Optional.empty(), 422);
|
||||
}
|
||||
|
@ -279,7 +280,12 @@ public class RdapNameserverSearchActionTest
|
|||
@Test
|
||||
public void testMultipleWildcards_rejected() {
|
||||
assertThat(generateActualJsonWithName("*.*"))
|
||||
.isEqualTo(generateExpectedJsonError("Only one wildcard allowed", 422));
|
||||
.isEqualTo(
|
||||
generateExpectedJsonError(
|
||||
"Query can only have a single wildcard, and it must be at the end of a label, but"
|
||||
+ " was: '*.*'",
|
||||
422));
|
||||
rememberWildcardTypeInvalid();
|
||||
assertThat(response.getStatus()).isEqualTo(422);
|
||||
verifyErrorMetrics(Optional.empty(), 422);
|
||||
}
|
||||
|
@ -337,7 +343,7 @@ public class RdapNameserverSearchActionTest
|
|||
action.registrarParam = Optional.of("unicoderegistrar");
|
||||
generateActualJsonWithName("ns1.cat.lol");
|
||||
assertThat(response.getStatus()).isEqualTo(404);
|
||||
verifyErrorMetrics(Optional.of(0L), 404);
|
||||
verifyErrorMetrics(Optional.of(1L), 404);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -373,13 +379,20 @@ public class RdapNameserverSearchActionTest
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testNameMatch_ns1_cat_idn_unicode_badRequest() {
|
||||
// name must use punycode.
|
||||
generateActualJsonWithName("ns1.cat.みんな");
|
||||
metricWildcardType = WildcardType.INVALID;
|
||||
metricPrefixLength = 0;
|
||||
assertThat(response.getStatus()).isEqualTo(400);
|
||||
verifyErrorMetrics(Optional.empty(), 400);
|
||||
public void testNameMatch_ns1_cat_idn_unicode_found() {
|
||||
assertThat(generateActualJsonWithName("ns1.cat.みんな"))
|
||||
.isEqualTo(
|
||||
generateExpectedJsonForNameserver(
|
||||
"ns1.cat.みんな",
|
||||
"ns1.cat.xn--q9jyb4c",
|
||||
"B-ROID",
|
||||
"v4",
|
||||
"1.2.3.5",
|
||||
"rdap_host_unicode.json"));
|
||||
metricWildcardType = WildcardType.NO_WILDCARD;
|
||||
metricPrefixLength = 19;
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
verifyMetrics(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -43,28 +43,32 @@ public class RdapSearchActionTestCase<A extends RdapSearchActionBase>
|
|||
public void initRdapSearchActionTestCase() {
|
||||
action.parameterMap = ImmutableListMultimap.of();
|
||||
action.cursorTokenParam = Optional.empty();
|
||||
action.registrarParam = Optional.empty();
|
||||
action.rdapResultSetMaxSize = 4;
|
||||
action.requestUrl = "https://example.tld" + actionPath;
|
||||
action.requestPath = actionPath;
|
||||
}
|
||||
|
||||
void rememberWildcardType(String queryString) {
|
||||
try {
|
||||
RdapSearchPattern partialStringQuery = RdapSearchPattern.create(queryString, true);
|
||||
if (!partialStringQuery.getHasWildcard()) {
|
||||
metricWildcardType = WildcardType.NO_WILDCARD;
|
||||
} else if (partialStringQuery.getSuffix() == null) {
|
||||
metricWildcardType = WildcardType.PREFIX;
|
||||
} else if (partialStringQuery.getInitialString().isEmpty()) {
|
||||
metricWildcardType = WildcardType.SUFFIX;
|
||||
void rememberWildcardType(WildcardType wildcardType, int prefixLength) {
|
||||
metricWildcardType = wildcardType;
|
||||
metricPrefixLength = prefixLength;
|
||||
}
|
||||
|
||||
void rememberWildcardType(String searchQuery) {
|
||||
int wildcardLocation = searchQuery.indexOf('*');
|
||||
if (wildcardLocation < 0) {
|
||||
rememberWildcardType(WildcardType.NO_WILDCARD, searchQuery.length());
|
||||
} else if (wildcardLocation == searchQuery.length() - 1) {
|
||||
rememberWildcardType(WildcardType.PREFIX, wildcardLocation);
|
||||
} else if (wildcardLocation == 0) {
|
||||
rememberWildcardType(WildcardType.SUFFIX, wildcardLocation);
|
||||
} else {
|
||||
metricWildcardType = WildcardType.PREFIX_AND_SUFFIX;
|
||||
rememberWildcardType(WildcardType.PREFIX_AND_SUFFIX, wildcardLocation);
|
||||
}
|
||||
metricPrefixLength = partialStringQuery.getInitialString().length();
|
||||
} catch (Exception e) {
|
||||
metricWildcardType = WildcardType.INVALID;
|
||||
metricPrefixLength = 0;
|
||||
}
|
||||
|
||||
void rememberWildcardTypeInvalid() {
|
||||
rememberWildcardType(WildcardType.INVALID, 0);
|
||||
}
|
||||
|
||||
void verifyMetrics(
|
||||
|
|
|
@ -28,7 +28,7 @@ public class RdapSearchPatternTest {
|
|||
|
||||
@Test
|
||||
public void testNoWildcards_ok() {
|
||||
RdapSearchPattern rdapSearchPattern = RdapSearchPattern.create("example.lol", true);
|
||||
RdapSearchPattern rdapSearchPattern = RdapSearchPattern.createFromLdhDomainName("example.lol");
|
||||
assertThat(rdapSearchPattern.getInitialString()).isEqualTo("example.lol");
|
||||
assertThat(rdapSearchPattern.getHasWildcard()).isFalse();
|
||||
assertThat(rdapSearchPattern.getSuffix()).isNull();
|
||||
|
@ -36,7 +36,7 @@ public class RdapSearchPatternTest {
|
|||
|
||||
@Test
|
||||
public void testWildcardNoTld_ok() {
|
||||
RdapSearchPattern rdapSearchPattern = RdapSearchPattern.create("exam*", true);
|
||||
RdapSearchPattern rdapSearchPattern = RdapSearchPattern.createFromLdhDomainName("exam*");
|
||||
assertThat(rdapSearchPattern.getInitialString()).isEqualTo("exam");
|
||||
assertThat(rdapSearchPattern.getHasWildcard()).isTrue();
|
||||
assertThat(rdapSearchPattern.getSuffix()).isNull();
|
||||
|
@ -44,32 +44,52 @@ public class RdapSearchPatternTest {
|
|||
|
||||
@Test
|
||||
public void testWildcardTld_ok() {
|
||||
RdapSearchPattern rdapSearchPattern = RdapSearchPattern.create("exam*.lol", true);
|
||||
RdapSearchPattern rdapSearchPattern = RdapSearchPattern.createFromLdhDomainName("exam*.lol");
|
||||
assertThat(rdapSearchPattern.getInitialString()).isEqualTo("exam");
|
||||
assertThat(rdapSearchPattern.getHasWildcard()).isTrue();
|
||||
assertThat(rdapSearchPattern.getSuffix()).isEqualTo("lol");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWildcardAtStart_ok() {
|
||||
RdapSearchPattern rdapSearchPattern = RdapSearchPattern.createFromLdhDomainName("*.lol");
|
||||
assertThat(rdapSearchPattern.getInitialString()).isEmpty();
|
||||
assertThat(rdapSearchPattern.getHasWildcard()).isTrue();
|
||||
assertThat(rdapSearchPattern.getSuffix()).isEqualTo("lol");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWildcardOnly_ok() {
|
||||
RdapSearchPattern rdapSearchPattern = RdapSearchPattern.createFromLdhDomainName("*");
|
||||
assertThat(rdapSearchPattern.getInitialString()).isEmpty();
|
||||
assertThat(rdapSearchPattern.getHasWildcard()).isTrue();
|
||||
assertThat(rdapSearchPattern.getSuffix()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleWildcards_unprocessable() {
|
||||
assertThrows(
|
||||
UnprocessableEntityException.class, () -> RdapSearchPattern.create("ex*am*.lol", true));
|
||||
UnprocessableEntityException.class,
|
||||
() -> RdapSearchPattern.createFromLdhDomainName("ex*am*.lol"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWildcardNotAtEnd_unprocessable() {
|
||||
assertThrows(UnprocessableEntityException.class, () -> RdapSearchPattern.create("ex*am", true));
|
||||
assertThrows(
|
||||
UnprocessableEntityException.class,
|
||||
() -> RdapSearchPattern.createFromLdhDomainName("ex*am"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWildcardNotAtEndWithTld_unprocessable() {
|
||||
assertThrows(
|
||||
UnprocessableEntityException.class, () -> RdapSearchPattern.create("ex*am.lol", true));
|
||||
UnprocessableEntityException.class,
|
||||
() -> RdapSearchPattern.createFromLdhDomainName("ex*am.lol"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortString_ok() {
|
||||
RdapSearchPattern rdapSearchPattern = RdapSearchPattern.create("e", true);
|
||||
RdapSearchPattern rdapSearchPattern = RdapSearchPattern.createFromLdhDomainName("e");
|
||||
assertThat(rdapSearchPattern.getInitialString()).isEqualTo("e");
|
||||
assertThat(rdapSearchPattern.getHasWildcard()).isFalse();
|
||||
assertThat(rdapSearchPattern.getSuffix()).isNull();
|
||||
|
@ -78,24 +98,45 @@ public class RdapSearchPatternTest {
|
|||
@Test
|
||||
public void testZeroLengthSuffix_unprocessable() {
|
||||
assertThrows(
|
||||
UnprocessableEntityException.class, () -> RdapSearchPattern.create("exam*.", true));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDisallowedSuffix_unprocessable() {
|
||||
assertThrows(
|
||||
UnprocessableEntityException.class, () -> RdapSearchPattern.create("exam*.lol", false));
|
||||
UnprocessableEntityException.class,
|
||||
() -> RdapSearchPattern.createFromLdhDomainName("exam*."));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNextInitialString_alpha() {
|
||||
RdapSearchPattern rdapSearchPattern = RdapSearchPattern.create("exam*.lol", true);
|
||||
RdapSearchPattern rdapSearchPattern = RdapSearchPattern.createFromLdhDomainName("exam*.lol");
|
||||
assertThat(rdapSearchPattern.getNextInitialString()).isEqualTo("exan");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNextInitialString_unicode() {
|
||||
RdapSearchPattern rdapSearchPattern = RdapSearchPattern.create("cat.みんな", true);
|
||||
assertThat(rdapSearchPattern.getNextInitialString()).isEqualTo("cat.みんに");
|
||||
public void testNextInitialString_unicode_translatedToPunycode() {
|
||||
RdapSearchPattern rdapSearchPattern =
|
||||
RdapSearchPattern.createFromLdhOrUnicodeDomainName("cat.みんな");
|
||||
assertThat(rdapSearchPattern.getNextInitialString()).isEqualTo("cat.xn--q9jyb4d");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnicodeString_noWildcard() {
|
||||
RdapSearchPattern rdapSearchPattern =
|
||||
RdapSearchPattern.createFromUnicodeString("unicode みんに string");
|
||||
assertThat(rdapSearchPattern.getInitialString()).isEqualTo("unicode みんに string");
|
||||
assertThat(rdapSearchPattern.getHasWildcard()).isFalse();
|
||||
assertThat(rdapSearchPattern.getSuffix()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnicodeString_withWildcard() {
|
||||
RdapSearchPattern rdapSearchPattern =
|
||||
RdapSearchPattern.createFromUnicodeString("unicode みんに string*");
|
||||
assertThat(rdapSearchPattern.getInitialString()).isEqualTo("unicode みんに string");
|
||||
assertThat(rdapSearchPattern.getHasWildcard()).isTrue();
|
||||
assertThat(rdapSearchPattern.getSuffix()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnicodeString_middleWildcard() {
|
||||
assertThrows(
|
||||
UnprocessableEntityException.class,
|
||||
() -> RdapSearchPattern.createFromLdhDomainName("unicode みんに *string"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,8 @@ import java.util.Objects;
|
|||
|
||||
public class RdapTestHelper {
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
private static final Gson GSON =
|
||||
new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
|
||||
|
||||
static JsonElement createJson(String... lines) {
|
||||
return GSON.fromJson(Joiner.on("\n").join(lines), JsonElement.class);
|
||||
|
@ -47,7 +48,7 @@ public class RdapTestHelper {
|
|||
}
|
||||
|
||||
private static JsonObject createTosNotice(String linkBase) {
|
||||
return new Gson().toJsonTree(
|
||||
return GSON.toJsonTree(
|
||||
ImmutableMap.of(
|
||||
"title", "RDAP Terms of Service",
|
||||
"description",
|
||||
|
@ -90,8 +91,7 @@ public class RdapTestHelper {
|
|||
|
||||
notices.add(createTosNotice(linkBase));
|
||||
notices.add(
|
||||
new Gson()
|
||||
.toJsonTree(
|
||||
GSON.toJsonTree(
|
||||
ImmutableMap.of(
|
||||
"description",
|
||||
ImmutableList.of(
|
||||
|
@ -107,16 +107,14 @@ public class RdapTestHelper {
|
|||
|
||||
notices.add(createTosNotice(linkBase));
|
||||
notices.add(
|
||||
new Gson()
|
||||
.toJsonTree(
|
||||
GSON.toJsonTree(
|
||||
ImmutableMap.of(
|
||||
"description",
|
||||
ImmutableList.of(
|
||||
"This response conforms to the RDAP Operational Profile for gTLD"
|
||||
+ " Registries and Registrars version 1.0"))));
|
||||
notices.add(
|
||||
new Gson()
|
||||
.toJsonTree(
|
||||
GSON.toJsonTree(
|
||||
ImmutableMap.of(
|
||||
"title",
|
||||
"Status Codes",
|
||||
|
@ -132,8 +130,7 @@ public class RdapTestHelper {
|
|||
"href", "https://icann.org/epp",
|
||||
"type", "text/html")))));
|
||||
notices.add(
|
||||
new Gson()
|
||||
.toJsonTree(
|
||||
GSON.toJsonTree(
|
||||
ImmutableMap.of(
|
||||
"title",
|
||||
"RDDS Inaccuracy Complaint Form",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"objectClassName": "entity",
|
||||
"handle": "%NAME%",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"objectClassName" : "entity",
|
||||
"handle" : "REDACTED FOR PRIVACY",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"objectClassName" : "entity",
|
||||
"handle" : "%NAME%",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"objectClassName" : "entity",
|
||||
"handle" : "%NAME%",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"objectClassName" : "entity",
|
||||
"handle" : "REDACTED FOR PRIVACY",
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
"handle": "%DOMAIN_HANDLE_1%",
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"status": [
|
||||
"client delete prohibited",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"objectClassName": "domain",
|
||||
"handle": "%DOMAIN_HANDLE_1%",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"objectClassName": "domain",
|
||||
"ldhName": "%DOMAIN_PUNYCODE_NAME_1%",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"objectClassName": "domain",
|
||||
"handle": "%DOMAIN_HANDLE_1%",
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
"handle": "%DOMAIN_HANDLE_1%",
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"status": [
|
||||
"client delete prohibited",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"objectClassName": "domain",
|
||||
"handle": "%DOMAIN_HANDLE_1%",
|
||||
|
|
|
@ -83,7 +83,8 @@
|
|||
],
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
|
|
|
@ -84,7 +84,8 @@
|
|||
],
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
|
|
|
@ -84,7 +84,8 @@
|
|||
],
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"domainSearchResults": [
|
||||
{
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
],
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices": [
|
||||
{
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
{
|
||||
"rdapConformance" : ["rdap_level_0", "icann_rdap_response_profile_0"],
|
||||
"rdapConformance" : [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
{
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
{
|
||||
"rdapConformance" : ["rdap_level_0", "icann_rdap_response_profile_0"],
|
||||
"rdapConformance" : [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
{
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"objectClassName": "nameserver",
|
||||
"handle": "%HANDLE%",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"status": [
|
||||
"active"
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"objectClassName": "nameserver",
|
||||
"handle": "%HANDLE%",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"objectClassName": "nameserver",
|
||||
"handle": "%HANDLE%",
|
||||
|
|
|
@ -142,9 +142,9 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"rdapConformance":
|
||||
[
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -110,6 +110,7 @@
|
|||
],
|
||||
"rdapConformance":[
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -101,7 +101,11 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"rdapConformance": ["rdap_level_0", "icann_rdap_response_profile_0" ],
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
{
|
||||
|
|
|
@ -116,7 +116,11 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"rdapConformance": ["rdap_level_0", "icann_rdap_response_profile_0" ],
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
{
|
||||
|
|
|
@ -69,7 +69,11 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"rdapConformance" : ["rdap_level_0", "icann_rdap_response_profile_0"],
|
||||
"rdapConformance" : [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
{
|
||||
|
|
|
@ -230,7 +230,11 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"rdapConformance": ["rdap_level_0", "icann_rdap_response_profile_0" ],
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
{
|
||||
|
|
|
@ -91,7 +91,8 @@
|
|||
],
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
|
|
|
@ -134,7 +134,11 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"rdapConformance" : ["rdap_level_0", "icann_rdap_response_profile_0"],
|
||||
"rdapConformance" : [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
{
|
||||
|
|
|
@ -234,7 +234,11 @@
|
|||
"roles": ["registrar"]
|
||||
}
|
||||
],
|
||||
"rdapConformance": ["rdap_level_0", "icann_rdap_response_profile_0" ],
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices":
|
||||
[
|
||||
{
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"objectClassName" : "entity",
|
||||
"handle" : "%NAME%",
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
{
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"objectClassName" : "entity",
|
||||
"handle" : "%NAME%",
|
||||
|
|
|
@ -230,7 +230,11 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"rdapConformance": ["rdap_level_0", "icann_rdap_response_profile_0" ],
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices":
|
||||
[
|
||||
{
|
||||
|
|
|
@ -134,7 +134,11 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"rdapConformance" : ["rdap_level_0", "icann_rdap_response_profile_0"],
|
||||
"rdapConformance" : [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
{
|
||||
|
|
|
@ -231,7 +231,11 @@
|
|||
"roles" : ["registrar"]
|
||||
}
|
||||
],
|
||||
"rdapConformance": ["rdap_level_0", "icann_rdap_response_profile_0" ],
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
{
|
||||
|
|
|
@ -234,7 +234,11 @@
|
|||
"roles" : ["registrar"]
|
||||
}
|
||||
],
|
||||
"rdapConformance": ["rdap_level_0", "icann_rdap_response_profile_0" ],
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
{
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"key":"value","rdapConformance":["rdap_level_0","icann_rdap_response_profile_0"],"notices":[{"title":"RDAP Terms of Service","links":[{"href":"https:\/\/www.registry.tld\/about\/rdap\/tos.html","rel":"alternate","type":"text\/html","value":"http:\/\/myserver.example.com\/help\/tos"}],"description":["By querying our Domain Database, you are agreeing to comply with these terms so please read them carefully.","Any information provided is 'as is' without any guarantee of accuracy.","Please do not misuse the Domain Database. It is intended solely for query-based access.","Don't use the Domain Database to allow, enable, or otherwise support the transmission of mass unsolicited, commercial advertising or solicitations.","Don't access our Domain Database through the use of high volume, automated electronic processes that send queries or data to the systems of any ICANN-accredited registrar.","You may only use the information contained in the Domain Database for lawful purposes.","Do not compile, repackage, disseminate, or otherwise use the information contained in the Domain Database in its entirety, or in any substantial portion, without our prior written permission.","We may retain certain details about queries to our Domain Database for the purposes of detecting and preventing misuse.","We reserve the right to restrict or deny your access to the database if we suspect that you have failed to comply with these terms.","We reserve the right to modify this agreement at any time."]}]}
|
||||
{"key":"value","rdapConformance":["rdap_level_0","icann_rdap_response_profile_0","icann_rdap_technical_implementation_guide_0"],"notices":[{"title":"RDAP Terms of Service","links":[{"href":"https:\/\/www.registry.tld\/about\/rdap\/tos.html","rel":"alternate","type":"text\/html","value":"http:\/\/myserver.example.com\/help\/tos"}],"description":["By querying our Domain Database, you are agreeing to comply with these terms so please read them carefully.","Any information provided is 'as is' without any guarantee of accuracy.","Please do not misuse the Domain Database. It is intended solely for query-based access.","Don't use the Domain Database to allow, enable, or otherwise support the transmission of mass unsolicited, commercial advertising or solicitations.","Don't access our Domain Database through the use of high volume, automated electronic processes that send queries or data to the systems of any ICANN-accredited registrar.","You may only use the information contained in the Domain Database for lawful purposes.","Do not compile, repackage, disseminate, or otherwise use the information contained in the Domain Database in its entirety, or in any substantial portion, without our prior written permission.","We may retain certain details about queries to our Domain Database for the purposes of detecting and preventing misuse.","We reserve the right to restrict or deny your access to the database if we suspect that you have failed to comply with these terms.","We reserve the right to modify this agreement at any time."]}]}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"key" : "value",
|
||||
"rdapConformance" :
|
||||
[
|
||||
"rdapConformance" : [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices" :
|
||||
[
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"key": "value",
|
||||
"rdapConformance":
|
||||
[
|
||||
"rdapConformance": [
|
||||
"rdap_level_0",
|
||||
"icann_rdap_response_profile_0"
|
||||
"icann_rdap_response_profile_0",
|
||||
"icann_rdap_technical_implementation_guide_0"
|
||||
],
|
||||
"notices":
|
||||
[
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue