formatOutputParam;
@Inject @Config("rdapResultSetMaxSize") int rdapResultSetMaxSize;
@Inject RdapMetrics rdapMetrics;
/** Builder for metric recording. */
final RdapMetrics.RdapMetricInformation.Builder metricInformationBuilder =
RdapMetrics.RdapMetricInformation.builder();
private final String humanReadableObjectTypeName;
/** Returns a string like "domain name" or "nameserver", used for error strings. */
final String getHumanReadableObjectTypeName() {
return humanReadableObjectTypeName;
}
/** The endpoint type used for recording metrics. */
private final EndpointType endpointType;
/** Returns the servlet action path; used to extract the search string from the incoming path. */
final String getActionPath() {
return getPathForAction(getClass());
}
RdapActionBase(String humanReadableObjectTypeName, EndpointType endpointType) {
this.humanReadableObjectTypeName = humanReadableObjectTypeName;
this.endpointType = endpointType;
}
/**
* 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
* building a map, to make sure that the request would return a 500 status if it were
* invoked using GET. So this field should usually be ignored, unless there's some
* expensive task required to create the map which will never result in a request failure.
* @return A map (probably containing nested maps and lists) with the final JSON response data.
*/
abstract ReplyPayloadBase getJsonObjectForResource(
String pathSearchString, boolean isHeadRequest);
@Override
public void run() {
metricInformationBuilder.setIncludeDeleted(includeDeletedParam.orElse(false));
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.
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(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);
setPayload(replyObject);
metricInformationBuilder.setStatusCode(SC_OK);
} catch (HttpException e) {
logger.atInfo().withCause(e).log("Error in RDAP");
setError(e.getResponseCode(), e.getResponseCodeString(), e.getMessage());
} catch (URISyntaxException | IllegalArgumentException e) {
logger.atInfo().withCause(e).log("Bad request in RDAP");
setError(SC_BAD_REQUEST, "Bad Request", "Not a valid " + getHumanReadableObjectTypeName());
} catch (RuntimeException e) {
setError(SC_INTERNAL_SERVER_ERROR, "Internal Server Error", "An error was encountered");
logger.atSevere().withCause(e).log("Exception encountered while processing RDAP command");
}
rdapMetrics.updateMetrics(metricInformationBuilder.build());
}
void setError(int status, String title, String description) {
metricInformationBuilder.setStatusCode(status);
response.setStatus(status);
try {
setPayload(ErrorResponse.create(status, title, description));
} catch (Exception ex) {
logger.atSevere().withCause(ex).log("Failed to create an error response.");
response.setPayload("");
}
}
void setPayload(ReplyPayloadBase replyObject) {
if (requestMethod == Action.Method.HEAD) {
return;
}
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.disableHtmlEscaping();
if (formatOutputParam.orElse(false)) {
gsonBuilder.setPrettyPrinting();
}
Gson gson = gsonBuilder.create();
TopLevelReplyObject topLevelObject =
TopLevelReplyObject.create(replyObject, rdapJsonFormatter.createTosNotice());
response.setPayload(gson.toJson(topLevelObject.toJson()));
}
/**
* Returns true if the query should include deleted items.
*
* This is true only if the request specified an includeDeleted parameter of true, AND is
* eligible to see deleted information. Admins can see all deleted information, while
* authenticated registrars can see only their own deleted information. Note that if this method
* returns true, it just means that some deleted information might be viewable. If this is a
* registrar request, the caller must still verify that the registrar can see each particular
* item by calling {@link RdapAuthorization#isAuthorizedForClientId}.
*/
boolean shouldIncludeDeleted() {
// If includeDeleted is not specified, or set to false, we don't need to go any further.
if (!includeDeletedParam.orElse(false)) {
return false;
}
// Return true if we *might* be allowed to view any deleted info, meaning we're either an admin
// or have access to at least one registrar's data
return rdapAuthorization.role() == RdapAuthorization.Role.ADMINISTRATOR
|| !rdapAuthorization.clientIds().isEmpty();
}
DeletedItemHandling getDeletedItemHandling() {
return shouldIncludeDeleted() ? DeletedItemHandling.INCLUDE : DeletedItemHandling.EXCLUDE;
}
/**
* Returns true if the request is authorized to see the resource.
*
*
This is true if the resource is not deleted, or the request wants to see deleted items, and
* is authorized to do so.
*/
boolean isAuthorized(EppResource eppResource) {
return getRequestTime().isBefore(eppResource.getDeletionTime())
|| (shouldIncludeDeleted()
&& rdapAuthorization.isAuthorizedForClientId(
eppResource.getPersistedCurrentSponsorClientId()));
}
/**
* Returns true if the registrar should be visible.
*
*
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 isAuthorized(Registrar registrar) {
return (registrar.isLiveAndPubliclyVisible()
|| (shouldIncludeDeleted()
&& rdapAuthorization.isAuthorizedForClientId(registrar.getClientId())));
}
String canonicalizeName(String name) {
name = canonicalizeDomainName(name);
if (name.endsWith(".")) {
name = name.substring(0, name.length() - 1);
}
return name;
}
/** Returns the DateTime this request took place. */
DateTime getRequestTime() {
return rdapJsonFormatter.getRequestTime();
}
}