Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ private static JsonElement toJsonElement(String name, Member member, Object obje
// According to RFC 9083 section 3, the syntax of dates and times is defined in RFC3339.
//
// According to RFC3339, we should use ISO8601, which is what DateTime.toString does!
return new JsonPrimitive(((DateTime) object).toString());
return new JsonPrimitive(object.toString());
}
if (object == null) {
return JsonNull.INSTANCE;
Expand Down
40 changes: 39 additions & 1 deletion core/src/main/java/google/registry/rdap/RdapActionBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,14 @@
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.Streams;
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.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.EppResource;
import google.registry.model.registrar.Registrar;
Expand All @@ -41,6 +45,7 @@
import google.registry.request.Parameter;
import google.registry.request.RequestMethod;
import google.registry.request.RequestPath;
import google.registry.request.RequestUrl;
import google.registry.request.Response;
import google.registry.util.Clock;
import jakarta.inject.Inject;
Expand Down Expand Up @@ -75,6 +80,7 @@ protected enum DeletedItemHandling {
@Inject Response response;
@Inject @RequestMethod Action.Method requestMethod;
@Inject @RequestPath String requestPath;
@Inject @RequestUrl String requestUrl;
@Inject RdapAuthorization rdapAuthorization;
@Inject RdapJsonFormatter rdapJsonFormatter;
@Inject @Parameter("includeDeleted") Optional<Boolean> includeDeletedParam;
Expand Down Expand Up @@ -198,7 +204,9 @@ void setPayload(ReplyPayloadBase replyObject) {
TopLevelReplyObject topLevelObject =
TopLevelReplyObject.create(replyObject, rdapJsonFormatter.createTosNotice());
Gson gson = formatOutputParam.orElse(false) ? FORMATTED_OUTPUT_GSON : GSON;
response.setPayload(gson.toJson(topLevelObject.toJson()));
JsonObject jsonResult = topLevelObject.toJson();
addLinkValuesRecursively(jsonResult);
response.setPayload(gson.toJson(jsonResult));
}

/**
Expand Down Expand Up @@ -264,4 +272,34 @@ DateTime getRequestTime() {
return rdapJsonFormatter.getRequestTime();
}

/**
* Adds a request-referencing "value" to each link object.
*
* <p>This is the "context URI" as described in RFC 8288. Basically, this contains a reference to
* the request URL that generated this RDAP response.
*
* <p>This is required per the RDAP February 2024 response profile sections 2.6.3 and 2.10, and
* the technical implementation guide sections 3.2 and 3.3.2.
*
* <p>We must do this here (instead of where the links are generated) because many of the links
* (e.g. terms of service) are static constants, and thus cannot by default know what the request
* URL was.
*/
private void addLinkValuesRecursively(JsonElement jsonElement) {
if (jsonElement instanceof JsonArray jsonArray) {
jsonArray.forEach(this::addLinkValuesRecursively);
} else if (jsonElement instanceof JsonObject jsonObject) {
if (jsonObject.get("links") instanceof JsonArray linksArray) {
addLinkValues(linksArray);
}
jsonObject.entrySet().forEach(entry -> addLinkValuesRecursively(entry.getValue()));
}
}

private void addLinkValues(JsonArray linksArray) {
Streams.stream(linksArray)
.map(JsonElement::getAsJsonObject)
.filter(o -> !o.has("value"))
.forEach(o -> o.addProperty("value", requestUrl));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public class RdapIcannStandardInformation {
+ " https://icann.org/epp")
.addLink(
Link.builder()
.setRel("alternate")
.setRel("glossary")
.setHref("https://icann.org/epp")
.setType("text/html")
.build())
Expand All @@ -57,7 +57,7 @@ public class RdapIcannStandardInformation {
.setDescription("URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf")
.addLink(
Link.builder()
.setRel("alternate")
.setRel("help")
.setHref("https://icann.org/wicf")
.setType("text/html")
.build())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ Notice createTosNotice() {
URI htmlUri = htmlBaseURI.resolve(rdapTosStaticUrl);
noticeBuilder.addLink(
Link.builder()
.setRel("alternate")
.setRel("terms-of-service")
.setHref(htmlUri.toString())
.setType("text/html")
.build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import google.registry.request.HttpException.UnprocessableEntityException;
import google.registry.request.Parameter;
import google.registry.request.ParameterMap;
import google.registry.request.RequestUrl;
import jakarta.inject.Inject;
import jakarta.persistence.criteria.CriteriaBuilder;
import java.io.UnsupportedEncodingException;
Expand All @@ -54,7 +53,6 @@ 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,6 @@ void testRuntimeException_returns500Error() {
assertThat(response.getStatus()).isEqualTo(500);
}

@Test
void testValidName_works() {
assertThat(generateActualJson("no.thing")).isEqualTo(loadJsonFile("rdapjson_toplevel.json"));
assertThat(response.getStatus()).isEqualTo(200);
}

@Test
void testContentType_rdapjson_utf8() {
generateActualJson("no.thing");
Expand Down
181 changes: 169 additions & 12 deletions core/src/test/java/google/registry/rdap/RdapActionBaseTestCase.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@
import static google.registry.request.Action.Method.HEAD;
import static org.mockito.Mockito.mock;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.persistence.transaction.JpaTestExtensions;
Expand All @@ -35,6 +40,7 @@
import google.registry.util.TypeUtils;
import java.util.HashMap;
import java.util.Optional;
import javax.annotation.Nullable;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension;
Expand All @@ -43,6 +49,7 @@
abstract class RdapActionBaseTestCase<A extends RdapActionBase> {

protected final FakeClock clock = new FakeClock(DateTime.parse("2000-01-01TZ"));
static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();

@RegisterExtension
final JpaIntegrationTestExtension jpa =
Expand Down Expand Up @@ -107,18 +114,13 @@ void loginAsAdmin() {
metricRole = ADMINISTRATOR;
}

JsonObject generateActualJson(String domainName) {
action.requestPath = actionPath + domainName;
action.requestMethod = GET;
action.run();
return RdapTestHelper.parseJsonObject(response.getPayload());
JsonObject generateActualJson(String name) {
return RdapTestHelper.parseJsonObject(runAction(name));
}

String generateHeadPayload(String domainName) {
action.requestPath = actionPath + domainName;
String generateHeadPayload(String name) {
action.requestMethod = HEAD;
action.run();
return response.getPayload();
return runAction(name);
}

JsonObject generateExpectedJsonError(String description, int code) {
Expand All @@ -138,16 +140,135 @@ JsonObject generateExpectedJsonError(String description, int code) {
"TITLE",
title,
"CODE",
String.valueOf(code));
String.valueOf(code),
"REQUEST_URL",
action.requestUrl);
}

JsonFileBuilder jsonFileBuilder() {
return new JsonFileBuilder(action.requestUrl);
}

private String runAction(String name) {
action.requestPath = actionPath + name;
action.requestUrl = "https://example.tld" + actionPath + name;
action.run();
return response.getPayload();
}

static JsonFileBuilder jsonFileBuilder() {
return new JsonFileBuilder();
JsonElement createTosNotice() {
return JsonParser.parseString(
"""
{
"title": "RDAP Terms of Service",
"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."
],
"links": [
{
"rel": "self",
"href": "https://example.tld/rdap/help/tos",
"type": "application/rdap+json",
"value": "%REQUEST_URL%"
},
{
"rel": "terms-of-service",
"href": "https://www.example.tld/about/rdap/tos.html",
"type": "text/html",
"value": "%REQUEST_URL%"
}
]
}
"""
.replaceAll("%REQUEST_URL%", action.requestUrl));
}

JsonObject addPermanentBoilerplateNotices(JsonObject jsonObject) {
if (!jsonObject.has("notices")) {
jsonObject.add("notices", new JsonArray());
}
JsonArray notices = jsonObject.getAsJsonArray("notices");
notices.add(createTosNotice());
notices.add(
JsonParser.parseString(
"""
{
"description": [
"This response conforms to the RDAP Operational Profile for gTLD Registries and Registrars \
version 1.0"
]
}
"""));
return jsonObject;
}

JsonObject addDomainBoilerplateNotices(JsonObject jsonObject) {
addPermanentBoilerplateNotices(jsonObject);
JsonArray notices = jsonObject.getAsJsonArray("notices");
notices.add(
JsonParser.parseString(
"""
{
"title": "Status Codes",
"description": [
"For more information on domain status codes, please visit https://icann.org/epp"
],
"links": [
{
"rel": "glossary",
"href": "https://icann.org/epp",
"type": "text/html",
"value": "%REQUEST_URL%"
}
]
}
"""
.replaceAll("%REQUEST_URL%", action.requestUrl)));
notices.add(
JsonParser.parseString(
"""
{
"title": "RDDS Inaccuracy Complaint Form",
"description": [
"URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf"
],
"links": [
{
"rel": "help",
"href": "https://icann.org/wicf",
"type": "text/html",
"value": "%REQUEST_URL%"
}
]
}
"""
.replaceAll("%REQUEST_URL%", action.requestUrl)));
return jsonObject;
}

protected static final class JsonFileBuilder {
private final HashMap<String, String> substitutions = new HashMap<>();

private JsonFileBuilder(String requestUrl) {
substitutions.put("REQUEST_URL", requestUrl);
}

public JsonObject load(String filename) {
return RdapTestHelper.loadJsonFile(filename, substitutions);
}
Expand All @@ -158,6 +279,14 @@ public JsonFileBuilder put(String key, String value) {
return this;
}

public JsonFileBuilder putAll(String... keysAndValues) {
checkArgument(keysAndValues.length % 2 == 0);
for (int i = 0; i < keysAndValues.length; i += 2) {
put(keysAndValues[i], keysAndValues[i + 1]);
}
return this;
}

public JsonFileBuilder put(String key, int index, String value) {
return put(String.format("%s%d", key, index), value);
}
Expand Down Expand Up @@ -189,10 +318,38 @@ JsonFileBuilder addRegistrar(String fullName) {
return putNext("REGISTRAR_FULL_NAME_", fullName);
}

JsonFileBuilder addFullRegistrar(
String handle, @Nullable String fullName, String status, @Nullable String address) {
if (fullName != null) {
putNext("REGISTRAR_FULLNAME_", fullName);
}
if (address != null) {
putNext("REGISTRAR_ADDRESS_", address);
}
return putNext("REGISTRAR_HANDLE_", handle, "STATUS_", status);
}

JsonFileBuilder addContact(String handle) {
return putNext("CONTACT_HANDLE_", handle);
}

JsonFileBuilder addFullContact(
String handle,
@Nullable String status,
@Nullable String fullName,
@Nullable String address) {
if (fullName != null) {
putNext("CONTACT_FULLNAME_", fullName);
}
if (address != null) {
putNext("CONTACT_ADDRESS_", address);
}
if (status != null) {
putNext("STATUS_", status);
}
return putNext("CONTACT_HANDLE_", handle);
}

JsonFileBuilder setNextQuery(String nextQuery) {
return put("NEXT_QUERY", nextQuery);
}
Expand Down
Loading