Use built-in Java URL connections instead of UrlFetchService (#1535)

- Use the standard HttpsURLConnection to write/read data
- Rewrite RdeReporter, Nordn*Action, and Marksdb classes and related
  tests to conform to the new format
- Remove FakeURLFetchService and ForwardingUrlFetchService as they weren't used
- Refactor UrlFetchException to UrlConnectionException
- Refactor UrlFetchUtils to UrlConnectionUtils

I will need to test this on Alpha. Fortunately the connections that
don't require auth (e.g. TMDB downloading) should be testable.
This commit is contained in:
gbrodman 2022-03-04 14:16:22 -05:00 committed by GitHub
parent cc4f2c0c52
commit 0d62ac0410
26 changed files with 507 additions and 584 deletions

View file

@ -43,7 +43,7 @@ import google.registry.rde.JSchModule;
import google.registry.request.Modules.DatastoreServiceModule; import google.registry.request.Modules.DatastoreServiceModule;
import google.registry.request.Modules.Jackson2Module; import google.registry.request.Modules.Jackson2Module;
import google.registry.request.Modules.NetHttpTransportModule; import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.URLFetchServiceModule; import google.registry.request.Modules.UrlConnectionServiceModule;
import google.registry.request.Modules.UrlFetchTransportModule; import google.registry.request.Modules.UrlFetchTransportModule;
import google.registry.request.Modules.UserServiceModule; import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule; import google.registry.request.auth.AuthModule;
@ -80,7 +80,7 @@ import javax.inject.Singleton;
ServerTridProviderModule.class, ServerTridProviderModule.class,
SheetsServiceModule.class, SheetsServiceModule.class,
StackdriverModule.class, StackdriverModule.class,
URLFetchServiceModule.class, UrlConnectionServiceModule.class,
UrlFetchTransportModule.class, UrlFetchTransportModule.class,
UserServiceModule.class, UserServiceModule.class,
VoidDnsWriterModule.class, VoidDnsWriterModule.class,

View file

@ -34,7 +34,6 @@ import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.privileges.secretmanager.SecretManagerModule; import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.request.Modules.Jackson2Module; import google.registry.request.Modules.Jackson2Module;
import google.registry.request.Modules.NetHttpTransportModule; import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.UrlFetchTransportModule;
import google.registry.request.Modules.UserServiceModule; import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule; import google.registry.request.auth.AuthModule;
import google.registry.ui.ConsoleDebug.ConsoleConfigModule; import google.registry.ui.ConsoleDebug.ConsoleConfigModule;
@ -65,7 +64,6 @@ import javax.inject.Singleton;
SecretManagerModule.class, SecretManagerModule.class,
ServerTridProviderModule.class, ServerTridProviderModule.class,
StackdriverModule.class, StackdriverModule.class,
UrlFetchTransportModule.class,
UserServiceModule.class, UserServiceModule.class,
UtilsModule.class UtilsModule.class
}) })

View file

@ -34,7 +34,6 @@ import google.registry.persistence.PersistenceModule;
import google.registry.privileges.secretmanager.SecretManagerModule; import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.request.Modules.Jackson2Module; import google.registry.request.Modules.Jackson2Module;
import google.registry.request.Modules.NetHttpTransportModule; import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.UrlFetchTransportModule;
import google.registry.request.Modules.UserServiceModule; import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule; import google.registry.request.auth.AuthModule;
import google.registry.util.UtilsModule; import google.registry.util.UtilsModule;
@ -62,7 +61,6 @@ import javax.inject.Singleton;
SecretManagerModule.class, SecretManagerModule.class,
ServerTridProviderModule.class, ServerTridProviderModule.class,
StackdriverModule.class, StackdriverModule.class,
UrlFetchTransportModule.class,
UserServiceModule.class, UserServiceModule.class,
UtilsModule.class UtilsModule.class
}) })

View file

@ -36,7 +36,6 @@ import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.request.Modules.DatastoreServiceModule; import google.registry.request.Modules.DatastoreServiceModule;
import google.registry.request.Modules.Jackson2Module; import google.registry.request.Modules.Jackson2Module;
import google.registry.request.Modules.NetHttpTransportModule; import google.registry.request.Modules.NetHttpTransportModule;
import google.registry.request.Modules.UrlFetchTransportModule;
import google.registry.request.Modules.UserServiceModule; import google.registry.request.Modules.UserServiceModule;
import google.registry.request.auth.AuthModule; import google.registry.request.auth.AuthModule;
import google.registry.util.UtilsModule; import google.registry.util.UtilsModule;
@ -66,7 +65,6 @@ import javax.inject.Singleton;
ServerTridProviderModule.class, ServerTridProviderModule.class,
StackdriverModule.class, StackdriverModule.class,
ToolsRequestComponentModule.class, ToolsRequestComponentModule.class,
UrlFetchTransportModule.class,
UserServiceModule.class, UserServiceModule.class,
UtilsModule.class UtilsModule.class
}) })

View file

@ -14,26 +14,24 @@
package google.registry.rde; package google.registry.rde;
import static com.google.appengine.api.urlfetch.FetchOptions.Builder.validateCertificate; import static google.registry.request.UrlConnectionUtils.getResponseBytes;
import static com.google.appengine.api.urlfetch.HTTPMethod.PUT; import static google.registry.request.UrlConnectionUtils.setBasicAuth;
import static com.google.common.io.BaseEncoding.base64; import static google.registry.request.UrlConnectionUtils.setPayload;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static google.registry.util.DomainNameUtils.canonicalizeDomainName; import static google.registry.util.DomainNameUtils.canonicalizeDomainName;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.appengine.api.urlfetch.HTTPHeader; import com.google.api.client.http.HttpMethods;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse; import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.Config;
import google.registry.keyring.api.KeyModule.Key; import google.registry.keyring.api.KeyModule.Key;
import google.registry.request.HttpException.InternalServerErrorException; import google.registry.request.HttpException.InternalServerErrorException;
import google.registry.request.UrlConnectionService;
import google.registry.util.Retrier; import google.registry.util.Retrier;
import google.registry.util.UrlFetchException; import google.registry.util.UrlConnectionException;
import google.registry.xjc.XjcXmlTransformer; import google.registry.xjc.XjcXmlTransformer;
import google.registry.xjc.iirdea.XjcIirdeaResponseElement; import google.registry.xjc.iirdea.XjcIirdeaResponseElement;
import google.registry.xjc.iirdea.XjcIirdeaResult; import google.registry.xjc.iirdea.XjcIirdeaResult;
@ -41,6 +39,7 @@ import google.registry.xjc.rdeheader.XjcRdeHeader;
import google.registry.xjc.rdereport.XjcRdeReportReport; import google.registry.xjc.rdereport.XjcRdeReportReport;
import google.registry.xml.XmlException; import google.registry.xml.XmlException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.URL; import java.net.URL;
@ -55,12 +54,15 @@ public class RdeReporter {
private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** @see <a href="http://tools.ietf.org/html/draft-lozano-icann-registry-interfaces-05#section-4"> /**
* ICANN Registry Interfaces - Interface details</a>*/ * @see <a href="http://tools.ietf.org/html/draft-lozano-icann-registry-interfaces-05#section-4">
private static final String REPORT_MIME = "text/xml"; * ICANN Registry Interfaces - Interface details</a>
*/
private static final MediaType MEDIA_TYPE = MediaType.XML_UTF_8;
@Inject Retrier retrier; @Inject Retrier retrier;
@Inject URLFetchService urlFetchService; @Inject UrlConnectionService urlConnectionService;
@Inject @Config("rdeReportUrlPrefix") String reportUrlPrefix; @Inject @Config("rdeReportUrlPrefix") String reportUrlPrefix;
@Inject @Key("icannReportingPassword") String password; @Inject @Key("icannReportingPassword") String password;
@Inject RdeReporter() {} @Inject RdeReporter() {}
@ -74,29 +76,24 @@ public class RdeReporter {
// Send a PUT request to ICANN's HTTPS server. // Send a PUT request to ICANN's HTTPS server.
URL url = makeReportUrl(header.getTld(), report.getId()); URL url = makeReportUrl(header.getTld(), report.getId());
String username = header.getTld() + "_ry"; String username = header.getTld() + "_ry";
String token = base64().encode(String.format("%s:%s", username, password).getBytes(UTF_8));
final HTTPRequest req = new HTTPRequest(url, PUT, validateCertificate().setDeadline(60d));
req.addHeader(new HTTPHeader(CONTENT_TYPE, REPORT_MIME));
req.addHeader(new HTTPHeader(AUTHORIZATION, "Basic " + token));
req.setPayload(reportBytes);
logger.atInfo().log("Sending report:\n%s", new String(reportBytes, UTF_8)); logger.atInfo().log("Sending report:\n%s", new String(reportBytes, UTF_8));
HTTPResponse rsp = byte[] responseBytes =
retrier.callWithRetry( retrier.callWithRetry(
() -> { () -> {
HTTPResponse rsp1 = urlFetchService.fetch(req); HttpURLConnection connection = urlConnectionService.createConnection(url);
switch (rsp1.getResponseCode()) { connection.setRequestMethod(HttpMethods.PUT);
case SC_OK: setBasicAuth(connection, username, password);
case SC_BAD_REQUEST: setPayload(connection, reportBytes, MEDIA_TYPE.toString());
break; int responseCode = connection.getResponseCode();
default: if (responseCode == SC_OK || responseCode == SC_BAD_REQUEST) {
throw new UrlFetchException("PUT failed", req, rsp1); return getResponseBytes(connection);
} }
return rsp1; throw new UrlConnectionException("PUT failed", connection);
}, },
SocketTimeoutException.class); SocketTimeoutException.class);
// Ensure the XML response is valid. // Ensure the XML response is valid.
XjcIirdeaResult result = parseResult(rsp); XjcIirdeaResult result = parseResult(responseBytes);
if (result.getCode().getValue() != 1000) { if (result.getCode().getValue() != 1000) {
logger.atWarning().log( logger.atWarning().log(
"PUT rejected: %d %s\n%s", "PUT rejected: %d %s\n%s",
@ -108,11 +105,11 @@ public class RdeReporter {
/** /**
* Unmarshals IIRDEA XML result object from {@link HTTPResponse} payload. * Unmarshals IIRDEA XML result object from {@link HTTPResponse} payload.
* *
* @see <a href="http://tools.ietf.org/html/draft-lozano-icann-registry-interfaces-05#section-4.1"> * @see <a
* href="http://tools.ietf.org/html/draft-lozano-icann-registry-interfaces-05#section-4.1">
* ICANN Registry Interfaces - IIRDEA Result Object</a> * ICANN Registry Interfaces - IIRDEA Result Object</a>
*/ */
private XjcIirdeaResult parseResult(HTTPResponse rsp) throws XmlException { private XjcIirdeaResult parseResult(byte[] responseBytes) throws XmlException {
byte[] responseBytes = rsp.getContent();
logger.atInfo().log("Received response:\n%s", new String(responseBytes, UTF_8)); logger.atInfo().log("Received response:\n%s", new String(responseBytes, UTF_8));
XjcIirdeaResponseElement response = XjcXmlTransformer.unmarshal( XjcIirdeaResponseElement response = XjcXmlTransformer.unmarshal(
XjcIirdeaResponseElement.class, new ByteArrayInputStream(responseBytes)); XjcIirdeaResponseElement.class, new ByteArrayInputStream(responseBytes));

View file

@ -23,12 +23,11 @@ import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory; import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
import com.google.appengine.api.users.UserService; import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory; import com.google.appengine.api.users.UserServiceFactory;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import java.net.HttpURLConnection;
import javax.inject.Singleton; import javax.inject.Singleton;
/** Dagger modules for App Engine services and other vendor classes. */ /** Dagger modules for App Engine services and other vendor classes. */
@ -45,14 +44,12 @@ public final class Modules {
} }
} }
/** Dagger module for {@link URLFetchService}. */ /** Dagger module for {@link UrlConnectionService}. */
@Module @Module
public static final class URLFetchServiceModule { public static final class UrlConnectionServiceModule {
private static final URLFetchService fetchService = URLFetchServiceFactory.getURLFetchService();
@Provides @Provides
static URLFetchService provideURLFetchService() { static UrlConnectionService provideUrlConnectionService() {
return fetchService; return url -> (HttpURLConnection) url.openConnection();
} }
} }

View file

@ -0,0 +1,25 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.request;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
/** Functional interface for opening a connection from a URL, injectable for testing. */
public interface UrlConnectionService {
HttpURLConnection createConnection(URL url) throws IOException;
}

View file

@ -1,4 +1,4 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved. // Copyright 2022 The Nomulus Authors. All Rights Reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package google.registry.util; package google.registry.request;
import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkState;
import static com.google.common.io.BaseEncoding.base64; import static com.google.common.io.BaseEncoding.base64;
@ -22,36 +22,41 @@ import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE; import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.common.base.Ascii;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.google.common.io.ByteStreams;
import com.google.common.net.MediaType; import com.google.common.net.MediaType;
import java.util.Optional; import java.io.DataOutputStream;
import java.io.IOException;
import java.net.URLConnection;
import java.util.Random; import java.util.Random;
/** Helper methods for the App Engine URL fetch service. */ /** Utilities for common functionality relating to {@link java.net.URLConnection}s. */
public final class UrlFetchUtils { public class UrlConnectionUtils {
/** Returns value of first header matching {@code name}. */ /** Retrieves the response from the given connection as a byte array. */
public static Optional<String> getHeaderFirst(HTTPResponse rsp, String name) { public static byte[] getResponseBytes(URLConnection connection) throws IOException {
return getHeaderFirstInternal(rsp.getHeadersUncombined(), name); return ByteStreams.toByteArray(connection.getInputStream());
} }
/** Returns value of first header matching {@code name}. */ /** Sets auth on the given connection with the given username/password. */
public static Optional<String> getHeaderFirst(HTTPRequest req, String name) { public static void setBasicAuth(URLConnection connection, String username, String password) {
return getHeaderFirstInternal(req.getHeaders(), name); setBasicAuth(connection, String.format("%s:%s", username, password));
} }
private static Optional<String> getHeaderFirstInternal(Iterable<HTTPHeader> hdrs, String name) { /** Sets auth on the given connection with the given string, formatted "username:password". */
name = Ascii.toLowerCase(name); public static void setBasicAuth(URLConnection connection, String usernameAndPassword) {
for (HTTPHeader header : hdrs) { String token = base64().encode(usernameAndPassword.getBytes(UTF_8));
if (Ascii.toLowerCase(header.getName()).equals(name)) { connection.setRequestProperty(AUTHORIZATION, "Basic " + token);
return Optional.of(header.getValue());
} }
/** Sets the given byte[] payload on the given connection with a particular content type. */
public static void setPayload(URLConnection connection, byte[] bytes, String contentType)
throws IOException {
connection.setRequestProperty(CONTENT_TYPE, contentType);
connection.setDoOutput(true);
try (DataOutputStream dataStream = new DataOutputStream(connection.getOutputStream())) {
dataStream.write(bytes);
} }
return Optional.empty();
} }
/** /**
@ -62,16 +67,16 @@ public final class UrlFetchUtils {
* @see <a href="http://www.ietf.org/rfc/rfc2388.txt">RFC2388 - Returning Values from Forms</a> * @see <a href="http://www.ietf.org/rfc/rfc2388.txt">RFC2388 - Returning Values from Forms</a>
*/ */
public static void setPayloadMultipart( public static void setPayloadMultipart(
HTTPRequest request, URLConnection connection,
String name, String name,
String filename, String filename,
MediaType contentType, MediaType contentType,
String data, String data,
Random random) { Random random)
throws IOException {
String boundary = createMultipartBoundary(random); String boundary = createMultipartBoundary(random);
checkState( checkState(
!data.contains(boundary), !data.contains(boundary), "Multipart data contains autogenerated boundary: %s", boundary);
"Multipart data contains autogenerated boundary: %s", boundary);
String multipart = String multipart =
String.format("--%s\r\n", boundary) String.format("--%s\r\n", boundary)
+ String.format( + String.format(
@ -83,11 +88,9 @@ public final class UrlFetchUtils {
+ "\r\n" + "\r\n"
+ String.format("--%s--\r\n", boundary); + String.format("--%s--\r\n", boundary);
byte[] payload = multipart.getBytes(UTF_8); byte[] payload = multipart.getBytes(UTF_8);
request.addHeader( connection.setRequestProperty(CONTENT_LENGTH, Integer.toString(payload.length));
new HTTPHeader( setPayload(
CONTENT_TYPE, String.format("multipart/form-data;" + " boundary=\"%s\"", boundary))); connection, payload, String.format("multipart/form-data;" + " boundary=\"%s\"", boundary));
request.addHeader(new HTTPHeader(CONTENT_LENGTH, Integer.toString(payload.length)));
request.setPayload(payload);
} }
private static String createMultipartBoundary(Random random) { private static String createMultipartBoundary(Random random) {
@ -98,12 +101,4 @@ public final class UrlFetchUtils {
// See https://tools.ietf.org/html/rfc2046#section-5.1.1 // See https://tools.ietf.org/html/rfc2046#section-5.1.1
return Strings.repeat("-", 30) + base64().encode(rand); return Strings.repeat("-", 30) + base64().encode(rand);
} }
/** Sets the HTTP Basic Authentication header on an {@link HTTPRequest}. */
public static void setAuthorizationHeader(HTTPRequest req, Optional<String> login) {
if (login.isPresent()) {
String token = base64().encode(login.get().getBytes(UTF_8));
req.addHeader(new HTTPHeader(AUTHORIZATION, "Basic " + token));
}
}
} }

View file

@ -15,12 +15,12 @@
package google.registry.tmch; package google.registry.tmch;
import static com.google.common.base.Verify.verifyNotNull; import static com.google.common.base.Verify.verifyNotNull;
import static google.registry.util.UrlFetchUtils.setAuthorizationHeader;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import google.registry.keyring.api.KeyModule.Key; import google.registry.keyring.api.KeyModule.Key;
import google.registry.model.tld.Registry; import google.registry.model.tld.Registry;
import google.registry.request.UrlConnectionUtils;
import java.net.HttpURLConnection;
import java.util.Optional; import java.util.Optional;
import javax.inject.Inject; import javax.inject.Inject;
@ -37,8 +37,9 @@ final class LordnRequestInitializer {
} }
/** Initializes a URL fetch request for talking to the MarksDB server. */ /** Initializes a URL fetch request for talking to the MarksDB server. */
void initialize(HTTPRequest request, String tld) { void initialize(HttpURLConnection connection, String tld) {
setAuthorizationHeader(request, getMarksDbLordnCredentials(tld)); getMarksDbLordnCredentials(tld)
.ifPresent(login -> UrlConnectionUtils.setBasicAuth(connection, login));
} }
/** Returns the username and password for the current TLD to login to the MarksDB server. */ /** Returns the username and password for the current TLD to login to the MarksDB server. */

View file

@ -14,25 +14,23 @@
package google.registry.tmch; package google.registry.tmch;
import static com.google.appengine.api.urlfetch.FetchOptions.Builder.validateCertificate;
import static com.google.appengine.api.urlfetch.HTTPMethod.GET;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.request.UrlConnectionUtils.getResponseBytes;
import static google.registry.request.UrlConnectionUtils.setBasicAuth;
import static google.registry.util.HexDumper.dumpHex; import static google.registry.util.HexDumper.dumpHex;
import static google.registry.util.UrlFetchUtils.setAuthorizationHeader;
import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.US_ASCII;
import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteSource; import com.google.common.io.ByteSource;
import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.Config;
import google.registry.keyring.api.KeyModule.Key; import google.registry.keyring.api.KeyModule.Key;
import google.registry.util.UrlFetchException; import google.registry.request.UrlConnectionService;
import google.registry.util.UrlConnectionException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.security.Security; import java.security.Security;
import java.security.SignatureException; import java.security.SignatureException;
@ -57,7 +55,8 @@ public final class Marksdb {
private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final int MAX_DNL_LOGGING_LENGTH = 500; private static final int MAX_DNL_LOGGING_LENGTH = 500;
@Inject URLFetchService fetchService; @Inject UrlConnectionService urlConnectionService;
@Inject @Config("tmchMarksdbUrl") String tmchMarksdbUrl; @Inject @Config("tmchMarksdbUrl") String tmchMarksdbUrl;
@Inject @Key("marksdbPublicKey") PGPPublicKey marksdbPublicKey; @Inject @Key("marksdbPublicKey") PGPPublicKey marksdbPublicKey;
@Inject Marksdb() {} @Inject Marksdb() {}
@ -112,19 +111,16 @@ public final class Marksdb {
} }
byte[] fetch(URL url, Optional<String> loginAndPassword) throws IOException { byte[] fetch(URL url, Optional<String> loginAndPassword) throws IOException {
HTTPRequest req = new HTTPRequest(url, GET, validateCertificate().setDeadline(60d)); HttpURLConnection connection = urlConnectionService.createConnection(url);
setAuthorizationHeader(req, loginAndPassword); loginAndPassword.ifPresent(auth -> setBasicAuth(connection, auth));
HTTPResponse rsp;
try { try {
rsp = fetchService.fetch(req); if (connection.getResponseCode() != SC_OK) {
throw new UrlConnectionException("Failed to fetch from MarksDB", connection);
}
return getResponseBytes(connection);
} catch (IOException e) { } catch (IOException e) {
throw new IOException( throw new IOException(String.format("Error connecting to MarksDB at URL %s", url), e);
String.format("Error connecting to MarksDB at URL %s", url), e);
} }
if (rsp.getResponseCode() != SC_OK) {
throw new UrlFetchException("Failed to fetch from MarksDB", req, rsp);
}
return rsp.getContent();
} }
List<String> fetchSignedCsv(Optional<String> loginAndPassword, String csvPath, String sigPath) List<String> fetchSignedCsv(Optional<String> loginAndPassword, String csvPath, String sigPath)

View file

@ -16,28 +16,23 @@ package google.registry.tmch;
import static com.google.appengine.api.taskqueue.QueueFactory.getQueue; import static com.google.appengine.api.taskqueue.QueueFactory.getQueue;
import static com.google.appengine.api.taskqueue.TaskOptions.Builder.withUrl; import static com.google.appengine.api.taskqueue.TaskOptions.Builder.withUrl;
import static com.google.appengine.api.urlfetch.FetchOptions.Builder.validateCertificate;
import static com.google.appengine.api.urlfetch.HTTPMethod.POST;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.net.HttpHeaders.LOCATION; import static com.google.common.net.HttpHeaders.LOCATION;
import static com.google.common.net.MediaType.CSV_UTF_8; import static com.google.common.net.MediaType.CSV_UTF_8;
import static google.registry.request.UrlConnectionUtils.getResponseBytes;
import static google.registry.tmch.LordnTaskUtils.COLUMNS_CLAIMS; import static google.registry.tmch.LordnTaskUtils.COLUMNS_CLAIMS;
import static google.registry.tmch.LordnTaskUtils.COLUMNS_SUNRISE; import static google.registry.tmch.LordnTaskUtils.COLUMNS_SUNRISE;
import static google.registry.util.UrlFetchUtils.getHeaderFirst;
import static google.registry.util.UrlFetchUtils.setPayloadMultipart;
import static java.nio.charset.StandardCharsets.US_ASCII; import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED; import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED;
import com.google.api.client.http.HttpMethods;
import com.google.appengine.api.taskqueue.LeaseOptions; import com.google.appengine.api.taskqueue.LeaseOptions;
import com.google.appengine.api.taskqueue.Queue; import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.TaskHandle; import com.google.appengine.api.taskqueue.TaskHandle;
import com.google.appengine.api.taskqueue.TaskOptions; import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.appengine.api.taskqueue.TransientFailureException; import com.google.appengine.api.taskqueue.TransientFailureException;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.apphosting.api.DeadlineExceededException; import com.google.apphosting.api.DeadlineExceededException;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.Strings; import com.google.common.base.Strings;
@ -49,16 +44,18 @@ import google.registry.config.RegistryConfig.Config;
import google.registry.request.Action; import google.registry.request.Action;
import google.registry.request.Parameter; import google.registry.request.Parameter;
import google.registry.request.RequestParameters; import google.registry.request.RequestParameters;
import google.registry.request.UrlConnectionService;
import google.registry.request.UrlConnectionUtils;
import google.registry.request.auth.Auth; import google.registry.request.auth.Auth;
import google.registry.util.Clock; import google.registry.util.Clock;
import google.registry.util.Retrier; import google.registry.util.Retrier;
import google.registry.util.TaskQueueUtils; import google.registry.util.TaskQueueUtils;
import google.registry.util.UrlFetchException; import google.registry.util.UrlConnectionException;
import java.io.IOException; import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.inject.Inject; import javax.inject.Inject;
@ -97,7 +94,8 @@ public final class NordnUploadAction implements Runnable {
@Inject Retrier retrier; @Inject Retrier retrier;
@Inject SecureRandom random; @Inject SecureRandom random;
@Inject LordnRequestInitializer lordnRequestInitializer; @Inject LordnRequestInitializer lordnRequestInitializer;
@Inject URLFetchService fetchService; @Inject UrlConnectionService urlConnectionService;
@Inject @Config("tmchMarksdbUrl") String tmchMarksdbUrl; @Inject @Config("tmchMarksdbUrl") String tmchMarksdbUrl;
@Inject @Parameter(LORDN_PHASE_PARAM) String phase; @Inject @Parameter(LORDN_PHASE_PARAM) String phase;
@Inject @Parameter(RequestParameters.PARAM_TLD) String tld; @Inject @Parameter(RequestParameters.PARAM_TLD) String tld;
@ -193,47 +191,48 @@ public final class NordnUploadAction implements Runnable {
* <p>Idempotency: If the exact same LORDN report is uploaded twice, the MarksDB server will * <p>Idempotency: If the exact same LORDN report is uploaded twice, the MarksDB server will
* return the same confirmation number. * return the same confirmation number.
* *
* @see <a href="http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.3"> * @see <a href="http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.3">TMCH
* TMCH functional specifications - LORDN File</a> * functional specifications - LORDN File</a>
*/ */
private void uploadCsvToLordn(String urlPath, String csvData) throws IOException { private void uploadCsvToLordn(String urlPath, String csvData) throws IOException {
String url = tmchMarksdbUrl + urlPath; String url = tmchMarksdbUrl + urlPath;
logger.atInfo().log( logger.atInfo().log(
"LORDN upload task %s: Sending to URL: %s ; data: %s", actionLogId, url, csvData); "LORDN upload task %s: Sending to URL: %s ; data: %s", actionLogId, url, csvData);
HTTPRequest req = new HTTPRequest(new URL(url), POST, validateCertificate().setDeadline(60d)); HttpURLConnection connection = urlConnectionService.createConnection(new URL(url));
lordnRequestInitializer.initialize(req, tld); connection.setRequestMethod(HttpMethods.POST);
setPayloadMultipart(req, "file", "claims.csv", CSV_UTF_8, csvData, random); lordnRequestInitializer.initialize(connection, tld);
HTTPResponse rsp; UrlConnectionUtils.setPayloadMultipart(
connection, "file", "claims.csv", CSV_UTF_8, csvData, random);
try { try {
rsp = fetchService.fetch(req); int responseCode = connection.getResponseCode();
} catch (IOException e) {
throw new IOException(
String.format("Error connecting to MarksDB at URL %s", url), e);
}
if (logger.atInfo().isEnabled()) { if (logger.atInfo().isEnabled()) {
String response = String responseContent = new String(getResponseBytes(connection), US_ASCII);
(rsp.getContent() == null) ? "(null)" : new String(rsp.getContent(), US_ASCII); if (responseContent.isEmpty()) {
responseContent = "(null)";
}
logger.atInfo().log( logger.atInfo().log(
"LORDN upload task %s response: HTTP response code %d, response data: %s", "LORDN upload task %s response: HTTP response code %d, response data: %s",
actionLogId, rsp.getResponseCode(), response); actionLogId, responseCode, responseContent);
} }
if (rsp.getResponseCode() != SC_ACCEPTED) { if (responseCode != SC_ACCEPTED) {
throw new UrlFetchException( throw new UrlConnectionException(
String.format( String.format(
"LORDN upload task %s error: Failed to upload LORDN claims to MarksDB", actionLogId), "LORDN upload task %s error: Failed to upload LORDN claims to MarksDB",
req, actionLogId),
rsp); connection);
} }
Optional<String> location = getHeaderFirst(rsp, LOCATION); String location = connection.getHeaderField(LOCATION);
if (!location.isPresent()) { if (location == null) {
throw new UrlFetchException( throw new UrlConnectionException(
String.format( String.format(
"LORDN upload task %s error: MarksDB failed to provide a Location header", "LORDN upload task %s error: MarksDB failed to provide a Location header",
actionLogId), actionLogId),
req, connection);
rsp); }
getQueue(NordnVerifyAction.QUEUE).add(makeVerifyTask(new URL(location)));
} catch (IOException e) {
throw new IOException(String.format("Error connecting to MarksDB at URL %s", url), e);
} }
getQueue(NordnVerifyAction.QUEUE).add(makeVerifyTask(new URL(location.get())));
} }
private TaskOptions makeVerifyTask(URL url) { private TaskOptions makeVerifyTask(URL url) {

View file

@ -14,15 +14,11 @@
package google.registry.tmch; package google.registry.tmch;
import static com.google.appengine.api.urlfetch.FetchOptions.Builder.validateCertificate; import static google.registry.request.UrlConnectionUtils.getResponseBytes;
import static com.google.appengine.api.urlfetch.HTTPMethod.GET;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import com.google.common.io.ByteSource; import com.google.common.io.ByteSource;
@ -32,9 +28,11 @@ import google.registry.request.HttpException.ConflictException;
import google.registry.request.Parameter; import google.registry.request.Parameter;
import google.registry.request.RequestParameters; import google.registry.request.RequestParameters;
import google.registry.request.Response; import google.registry.request.Response;
import google.registry.request.UrlConnectionService;
import google.registry.request.auth.Auth; import google.registry.request.auth.Auth;
import google.registry.util.UrlFetchException; import google.registry.util.UrlConnectionException;
import java.io.IOException; import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.Map.Entry; import java.util.Map.Entry;
import javax.inject.Inject; import javax.inject.Inject;
@ -68,7 +66,8 @@ public final class NordnVerifyAction implements Runnable {
@Inject LordnRequestInitializer lordnRequestInitializer; @Inject LordnRequestInitializer lordnRequestInitializer;
@Inject Response response; @Inject Response response;
@Inject URLFetchService fetchService; @Inject UrlConnectionService urlConnectionService;
@Inject @Header(URL_HEADER) URL url; @Inject @Header(URL_HEADER) URL url;
@Inject @Header(HEADER_ACTION_LOG_ID) String actionLogId; @Inject @Header(HEADER_ACTION_LOG_ID) String actionLogId;
@Inject @Parameter(RequestParameters.PARAM_TLD) String tld; @Inject @Parameter(RequestParameters.PARAM_TLD) String tld;
@ -96,30 +95,25 @@ public final class NordnVerifyAction implements Runnable {
@VisibleForTesting @VisibleForTesting
LordnLog verify() throws IOException { LordnLog verify() throws IOException {
logger.atInfo().log("LORDN verify task %s: Sending request to URL %s", actionLogId, url); logger.atInfo().log("LORDN verify task %s: Sending request to URL %s", actionLogId, url);
HTTPRequest req = new HTTPRequest(url, GET, validateCertificate().setDeadline(60d)); HttpURLConnection connection = urlConnectionService.createConnection(url);
lordnRequestInitializer.initialize(req, tld); lordnRequestInitializer.initialize(connection, tld);
HTTPResponse rsp;
try { try {
rsp = fetchService.fetch(req); int responseCode = connection.getResponseCode();
} catch (IOException e) {
throw new IOException(
String.format("Error connecting to MarksDB at URL %s", url), e);
}
logger.atInfo().log( logger.atInfo().log(
"LORDN verify task %s response: HTTP response code %d, response data: %s", "LORDN verify task %s response: HTTP response code %d", actionLogId, responseCode);
actionLogId, rsp.getResponseCode(), rsp.getContent()); if (responseCode == SC_NO_CONTENT) {
if (rsp.getResponseCode() == SC_NO_CONTENT) {
// Send a 400+ status code so App Engine will retry the task. // Send a 400+ status code so App Engine will retry the task.
throw new ConflictException("Not ready"); throw new ConflictException("Not ready");
} }
if (rsp.getResponseCode() != SC_OK) { if (responseCode != SC_OK) {
throw new UrlFetchException( throw new UrlConnectionException(
String.format("LORDN verify task %s: Failed to verify LORDN upload to MarksDB.", String.format(
actionLogId), "LORDN verify task %s: Failed to verify LORDN upload to MarksDB.", actionLogId),
req, rsp); connection);
} }
LordnLog log = LordnLog log =
LordnLog.parse(ByteSource.wrap(rsp.getContent()).asCharSource(UTF_8).readLines()); LordnLog.parse(
ByteSource.wrap(getResponseBytes(connection)).asCharSource(UTF_8).readLines());
if (log.getStatus() == LordnLog.Status.ACCEPTED) { if (log.getStatus() == LordnLog.Status.ACCEPTED) {
logger.atInfo().log("LORDN verify task %s: Upload accepted.", actionLogId); logger.atInfo().log("LORDN verify task %s: Upload accepted.", actionLogId);
} else { } else {
@ -142,5 +136,8 @@ public final class NordnVerifyAction implements Runnable {
} }
} }
return log; return log;
} catch (IOException e) {
throw new IOException(String.format("Error connecting to MarksDB at URL %s", url), e);
}
} }
} }

View file

@ -38,8 +38,7 @@ import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.rde.RdeModule; import google.registry.rde.RdeModule;
import google.registry.request.Modules.DatastoreServiceModule; import google.registry.request.Modules.DatastoreServiceModule;
import google.registry.request.Modules.Jackson2Module; import google.registry.request.Modules.Jackson2Module;
import google.registry.request.Modules.URLFetchServiceModule; import google.registry.request.Modules.UrlConnectionServiceModule;
import google.registry.request.Modules.UrlFetchTransportModule;
import google.registry.request.Modules.UserServiceModule; import google.registry.request.Modules.UserServiceModule;
import google.registry.tools.AuthModule.LocalCredentialModule; import google.registry.tools.AuthModule.LocalCredentialModule;
import google.registry.tools.javascrap.CompareEscrowDepositsCommand; import google.registry.tools.javascrap.CompareEscrowDepositsCommand;
@ -79,8 +78,7 @@ import javax.inject.Singleton;
RegistryToolDataflowModule.class, RegistryToolDataflowModule.class,
RequestFactoryModule.class, RequestFactoryModule.class,
SecretManagerModule.class, SecretManagerModule.class,
URLFetchServiceModule.class, UrlConnectionServiceModule.class,
UrlFetchTransportModule.class,
UserServiceModule.class, UserServiceModule.class,
UtilsModule.class, UtilsModule.class,
VoidDnsWriterModule.class, VoidDnsWriterModule.class,

View file

@ -14,7 +14,6 @@
package google.registry.rde; package google.registry.rde;
import static com.google.appengine.api.urlfetch.HTTPMethod.PUT;
import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8; import static com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.Cursor.CursorType.RDE_REPORT; import static google.registry.model.common.Cursor.CursorType.RDE_REPORT;
@ -29,20 +28,13 @@ import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.joda.time.Duration.standardDays; import static org.joda.time.Duration.standardDays;
import static org.joda.time.Duration.standardSeconds; import static org.joda.time.Duration.standardSeconds;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.cloud.storage.BlobId; import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper; import com.google.cloud.storage.contrib.nio.testing.LocalStorageHelper;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteSource; import com.google.common.io.ByteSource;
import google.registry.gcs.GcsUtils; import google.registry.gcs.GcsUtils;
import google.registry.model.common.Cursor; import google.registry.model.common.Cursor;
@ -57,20 +49,21 @@ import google.registry.testing.FakeClock;
import google.registry.testing.FakeKeyringModule; import google.registry.testing.FakeKeyringModule;
import google.registry.testing.FakeResponse; import google.registry.testing.FakeResponse;
import google.registry.testing.FakeSleeper; import google.registry.testing.FakeSleeper;
import google.registry.testing.FakeUrlConnectionService;
import google.registry.testing.TestOfyAndSql; import google.registry.testing.TestOfyAndSql;
import google.registry.util.Retrier; import google.registry.util.Retrier;
import google.registry.xjc.XjcXmlTransformer; import google.registry.xjc.XjcXmlTransformer;
import google.registry.xjc.rdereport.XjcRdeReportReport; import google.registry.xjc.rdereport.XjcRdeReportReport;
import google.registry.xml.XmlException; import google.registry.xml.XmlException;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKey;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor;
/** Unit tests for {@link RdeReportAction}. */ /** Unit tests for {@link RdeReportAction}. */
@DualDatabaseTest @DualDatabaseTest
@ -89,20 +82,21 @@ public class RdeReportActionTest {
private final FakeResponse response = new FakeResponse(); private final FakeResponse response = new FakeResponse();
private final EscrowTaskRunner runner = mock(EscrowTaskRunner.class); private final EscrowTaskRunner runner = mock(EscrowTaskRunner.class);
private final URLFetchService urlFetchService = mock(URLFetchService.class);
private final ArgumentCaptor<HTTPRequest> request = ArgumentCaptor.forClass(HTTPRequest.class);
private final HTTPResponse httpResponse = mock(HTTPResponse.class);
private final PGPPublicKey encryptKey = private final PGPPublicKey encryptKey =
new FakeKeyringModule().get().getRdeStagingEncryptionKey(); new FakeKeyringModule().get().getRdeStagingEncryptionKey();
private final GcsUtils gcsUtils = new GcsUtils(LocalStorageHelper.getOptions()); private final GcsUtils gcsUtils = new GcsUtils(LocalStorageHelper.getOptions());
private final BlobId reportFile = private final BlobId reportFile =
BlobId.of("tub", "test_2006-06-06_full_S1_R0-report.xml.ghostryde"); BlobId.of("tub", "test_2006-06-06_full_S1_R0-report.xml.ghostryde");
private final HttpURLConnection httpUrlConnection = mock(HttpURLConnection.class);
private final FakeUrlConnectionService urlConnectionService =
new FakeUrlConnectionService(httpUrlConnection);
private final ByteArrayOutputStream connectionOutputStream = new ByteArrayOutputStream();
private RdeReportAction createAction() { private RdeReportAction createAction() {
RdeReporter reporter = new RdeReporter(); RdeReporter reporter = new RdeReporter();
reporter.reportUrlPrefix = "https://rde-report.example"; reporter.reportUrlPrefix = "https://rde-report.example";
reporter.urlFetchService = urlFetchService;
reporter.password = "foo"; reporter.password = "foo";
reporter.urlConnectionService = urlConnectionService;
reporter.retrier = new Retrier(new FakeSleeper(new FakeClock()), 3); reporter.retrier = new Retrier(new FakeSleeper(new FakeClock()), 3);
RdeReportAction action = new RdeReportAction(); RdeReportAction action = new RdeReportAction();
action.gcsUtils = gcsUtils; action.gcsUtils = gcsUtils;
@ -127,6 +121,7 @@ public class RdeReportActionTest {
Cursor.create(RDE_UPLOAD, DateTime.parse("2006-06-07TZ"), Registry.get("test"))); Cursor.create(RDE_UPLOAD, DateTime.parse("2006-06-07TZ"), Registry.get("test")));
gcsUtils.createFromBytes(reportFile, Ghostryde.encode(REPORT_XML.read(), encryptKey)); gcsUtils.createFromBytes(reportFile, Ghostryde.encode(REPORT_XML.read(), encryptKey));
tm().transact(() -> RdeRevision.saveRevision("test", DateTime.parse("2006-06-06TZ"), FULL, 0)); tm().transact(() -> RdeRevision.saveRevision("test", DateTime.parse("2006-06-06TZ"), FULL, 0));
when(httpUrlConnection.getOutputStream()).thenReturn(connectionOutputStream);
} }
@TestOfyAndSql @TestOfyAndSql
@ -142,24 +137,22 @@ public class RdeReportActionTest {
@TestOfyAndSql @TestOfyAndSql
void testRunWithLock() throws Exception { void testRunWithLock() throws Exception {
when(httpResponse.getResponseCode()).thenReturn(SC_OK); when(httpUrlConnection.getResponseCode()).thenReturn(SC_OK);
when(httpResponse.getContent()).thenReturn(IIRDEA_GOOD_XML.read()); when(httpUrlConnection.getInputStream()).thenReturn(IIRDEA_GOOD_XML.openStream());
when(urlFetchService.fetch(request.capture())).thenReturn(httpResponse);
createAction().runWithLock(loadRdeReportCursor()); createAction().runWithLock(loadRdeReportCursor());
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8); assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8);
assertThat(response.getPayload()).isEqualTo("OK test 2006-06-06T00:00:00.000Z\n"); assertThat(response.getPayload()).isEqualTo("OK test 2006-06-06T00:00:00.000Z\n");
// Verify the HTTP request was correct. // Verify the HTTP request was correct.
assertThat(request.getValue().getMethod()).isSameInstanceAs(PUT); verify(httpUrlConnection).setRequestMethod("PUT");
assertThat(request.getValue().getURL().getProtocol()).isEqualTo("https"); assertThat(httpUrlConnection.getURL().getProtocol()).isEqualTo("https");
assertThat(request.getValue().getURL().getPath()).endsWith("/test/20101017001"); assertThat(httpUrlConnection.getURL().getPath()).endsWith("/test/20101017001");
Map<String, String> headers = mapifyHeaders(request.getValue().getHeaders()); verify(httpUrlConnection).setRequestProperty("Content-Type", "text/xml; charset=utf-8");
assertThat(headers).containsEntry("CONTENT_TYPE", "text/xml"); verify(httpUrlConnection).setRequestProperty("Authorization", "Basic dGVzdF9yeTpmb28=");
assertThat(headers).containsEntry("AUTHORIZATION", "Basic dGVzdF9yeTpmb28=");
// Verify the payload XML was the same as what's in testdata/report.xml. // Verify the payload XML was the same as what's in testdata/report.xml.
XjcRdeReportReport report = parseReport(request.getValue().getPayload()); XjcRdeReportReport report = parseReport(connectionOutputStream.toByteArray());
assertThat(report.getId()).isEqualTo("20101017001"); assertThat(report.getId()).isEqualTo("20101017001");
assertThat(report.getCrDate()).isEqualTo(DateTime.parse("2010-10-17T00:15:00.0Z")); assertThat(report.getCrDate()).isEqualTo(DateTime.parse("2010-10-17T00:15:00.0Z"));
assertThat(report.getWatermark()).isEqualTo(DateTime.parse("2010-10-17T00:00:00Z")); assertThat(report.getWatermark()).isEqualTo(DateTime.parse("2010-10-17T00:00:00Z"));
@ -167,9 +160,8 @@ public class RdeReportActionTest {
@TestOfyAndSql @TestOfyAndSql
void testRunWithLock_withPrefix() throws Exception { void testRunWithLock_withPrefix() throws Exception {
when(httpResponse.getResponseCode()).thenReturn(SC_OK); when(httpUrlConnection.getResponseCode()).thenReturn(SC_OK);
when(httpResponse.getContent()).thenReturn(IIRDEA_GOOD_XML.read()); when(httpUrlConnection.getInputStream()).thenReturn(IIRDEA_GOOD_XML.openStream());
when(urlFetchService.fetch(request.capture())).thenReturn(httpResponse);
RdeReportAction action = createAction(); RdeReportAction action = createAction();
action.prefix = Optional.of("job-name/"); action.prefix = Optional.of("job-name/");
gcsUtils.delete(reportFile); gcsUtils.delete(reportFile);
@ -182,16 +174,14 @@ public class RdeReportActionTest {
assertThat(response.getPayload()).isEqualTo("OK test 2006-06-06T00:00:00.000Z\n"); assertThat(response.getPayload()).isEqualTo("OK test 2006-06-06T00:00:00.000Z\n");
// Verify the HTTP request was correct. // Verify the HTTP request was correct.
assertThat(request.getValue().getMethod()).isSameInstanceAs(PUT); verify(httpUrlConnection).setRequestMethod("PUT");
assertThat(request.getValue().getURL().getProtocol()).isEqualTo("https"); assertThat(httpUrlConnection.getURL().getProtocol()).isEqualTo("https");
assertThat(request.getValue().getURL().getPath()).endsWith("/test/20101017001"); assertThat(httpUrlConnection.getURL().getPath()).endsWith("/test/20101017001");
Map<String, String> headers = mapifyHeaders(request.getValue().getHeaders()); verify(httpUrlConnection).setRequestProperty("Content-Type", "text/xml; charset=utf-8");
assertThat(headers).containsEntry("CONTENT_TYPE", "text/xml"); verify(httpUrlConnection).setRequestProperty("Authorization", "Basic dGVzdF9yeTpmb28=");
assertThat(headers)
.containsEntry("AUTHORIZATION", "Basic dGVzdF9yeTpmb28=");
// Verify the payload XML was the same as what's in testdata/report.xml. // Verify the payload XML was the same as what's in testdata/report.xml.
XjcRdeReportReport report = parseReport(request.getValue().getPayload()); XjcRdeReportReport report = parseReport(connectionOutputStream.toByteArray());
assertThat(report.getId()).isEqualTo("20101017001"); assertThat(report.getId()).isEqualTo("20101017001");
assertThat(report.getCrDate()).isEqualTo(DateTime.parse("2010-10-17T00:15:00.0Z")); assertThat(report.getCrDate()).isEqualTo(DateTime.parse("2010-10-17T00:15:00.0Z"));
assertThat(report.getWatermark()).isEqualTo(DateTime.parse("2010-10-17T00:00:00Z")); assertThat(report.getWatermark()).isEqualTo(DateTime.parse("2010-10-17T00:00:00Z"));
@ -204,9 +194,8 @@ public class RdeReportActionTest {
PGPPublicKey encryptKey = new FakeKeyringModule().get().getRdeStagingEncryptionKey(); PGPPublicKey encryptKey = new FakeKeyringModule().get().getRdeStagingEncryptionKey();
gcsUtils.createFromBytes(newReport, Ghostryde.encode(REPORT_XML.read(), encryptKey)); gcsUtils.createFromBytes(newReport, Ghostryde.encode(REPORT_XML.read(), encryptKey));
tm().transact(() -> RdeRevision.saveRevision("test", DateTime.parse("2006-06-06TZ"), FULL, 1)); tm().transact(() -> RdeRevision.saveRevision("test", DateTime.parse("2006-06-06TZ"), FULL, 1));
when(httpResponse.getResponseCode()).thenReturn(SC_OK); when(httpUrlConnection.getResponseCode()).thenReturn(SC_OK);
when(httpResponse.getContent()).thenReturn(IIRDEA_GOOD_XML.read()); when(httpUrlConnection.getInputStream()).thenReturn(IIRDEA_GOOD_XML.openStream());
when(urlFetchService.fetch(request.capture())).thenReturn(httpResponse);
createAction().runWithLock(loadRdeReportCursor()); createAction().runWithLock(loadRdeReportCursor());
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
} }
@ -239,9 +228,8 @@ public class RdeReportActionTest {
@TestOfyAndSql @TestOfyAndSql
void testRunWithLock_badRequest_throws500WithErrorInfo() throws Exception { void testRunWithLock_badRequest_throws500WithErrorInfo() throws Exception {
when(httpResponse.getResponseCode()).thenReturn(SC_BAD_REQUEST); when(httpUrlConnection.getResponseCode()).thenReturn(SC_BAD_REQUEST);
when(httpResponse.getContent()).thenReturn(IIRDEA_BAD_XML.read()); when(httpUrlConnection.getInputStream()).thenReturn(IIRDEA_BAD_XML.openStream());
when(urlFetchService.fetch(request.capture())).thenReturn(httpResponse);
InternalServerErrorException thrown = InternalServerErrorException thrown =
assertThrows( assertThrows(
InternalServerErrorException.class, InternalServerErrorException.class,
@ -252,18 +240,17 @@ public class RdeReportActionTest {
@TestOfyAndSql @TestOfyAndSql
void testRunWithLock_fetchFailed_throwsRuntimeException() throws Exception { void testRunWithLock_fetchFailed_throwsRuntimeException() throws Exception {
class ExpectedThrownException extends RuntimeException {} class ExpectedThrownException extends RuntimeException {}
when(urlFetchService.fetch(any(HTTPRequest.class))).thenThrow(new ExpectedThrownException()); when(httpUrlConnection.getResponseCode()).thenThrow(new ExpectedThrownException());
assertThrows( assertThrows(
ExpectedThrownException.class, () -> createAction().runWithLock(loadRdeReportCursor())); ExpectedThrownException.class, () -> createAction().runWithLock(loadRdeReportCursor()));
} }
@TestOfyAndSql @TestOfyAndSql
void testRunWithLock_socketTimeout_doesRetry() throws Exception { void testRunWithLock_socketTimeout_doesRetry() throws Exception {
when(httpResponse.getResponseCode()).thenReturn(SC_OK); when(httpUrlConnection.getInputStream()).thenReturn(IIRDEA_GOOD_XML.openStream());
when(httpResponse.getContent()).thenReturn(IIRDEA_GOOD_XML.read()); when(httpUrlConnection.getResponseCode())
when(urlFetchService.fetch(request.capture()))
.thenThrow(new SocketTimeoutException()) .thenThrow(new SocketTimeoutException())
.thenReturn(httpResponse); .thenReturn(SC_OK);
createAction().runWithLock(loadRdeReportCursor()); createAction().runWithLock(loadRdeReportCursor());
assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8); assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8);
@ -274,14 +261,6 @@ public class RdeReportActionTest {
return loadByKey(Cursor.createVKey(RDE_REPORT, "test")).getCursorTime(); return loadByKey(Cursor.createVKey(RDE_REPORT, "test")).getCursorTime();
} }
private static ImmutableMap<String, String> mapifyHeaders(Iterable<HTTPHeader> headers) {
ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>();
for (HTTPHeader header : headers) {
builder.put(Ascii.toUpperCase(header.getName().replace('-', '_')), header.getValue());
}
return builder.build();
}
private static XjcRdeReportReport parseReport(byte[] data) { private static XjcRdeReportReport parseReport(byte[] data) {
try { try {
return XjcXmlTransformer.unmarshal(XjcRdeReportReport.class, new ByteArrayInputStream(data)); return XjcXmlTransformer.unmarshal(XjcRdeReportReport.class, new ByteArrayInputStream(data));

View file

@ -1,4 +1,4 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved. // Copyright 2022 The Nomulus Authors. All Rights Reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -12,13 +12,13 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package google.registry.util; package google.registry.request;
import static com.google.common.net.HttpHeaders.CONTENT_LENGTH; import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;
import static com.google.common.net.HttpHeaders.CONTENT_TYPE; import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.common.net.MediaType.CSV_UTF_8; import static com.google.common.net.MediaType.CSV_UTF_8;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static google.registry.util.UrlFetchUtils.setPayloadMultipart; import static google.registry.request.UrlConnectionUtils.setPayloadMultipart;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@ -27,22 +27,19 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.appengine.api.urlfetch.HTTPHeader; import java.io.ByteArrayOutputStream;
import com.google.appengine.api.urlfetch.HTTPRequest;
import google.registry.testing.AppEngineExtension;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Random; import java.util.Random;
import javax.net.ssl.HttpsURLConnection;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
/** Unit tests for {@link UrlFetchUtils}. */ /** Tests for {@link UrlConnectionUtils}. */
class UrlFetchUtilsTest { public class UrlConnectionUtilsTest {
@RegisterExtension final AppEngineExtension appEngine = AppEngineExtension.builder().build();
private final Random random = mock(Random.class); private final Random random = mock(Random.class);
@ -58,25 +55,28 @@ class UrlFetchUtilsTest {
} }
@Test @Test
void testSetPayloadMultipart() { void testSetPayloadMultipart() throws Exception {
HTTPRequest request = mock(HTTPRequest.class); HttpsURLConnection connection = mock(HttpsURLConnection.class);
ByteArrayOutputStream connectionOutputStream = new ByteArrayOutputStream();
when(connection.getOutputStream()).thenReturn(connectionOutputStream);
setPayloadMultipart( setPayloadMultipart(
request, connection,
"lol", "lol",
"cat", "cat",
CSV_UTF_8, CSV_UTF_8,
"The nice people at the store say hello. ヘ(◕。◕ヘ)", "The nice people at the store say hello. ヘ(◕。◕ヘ)",
random); random);
ArgumentCaptor<HTTPHeader> headerCaptor = ArgumentCaptor.forClass(HTTPHeader.class); ArgumentCaptor<String> keyCaptor = ArgumentCaptor.forClass(String.class);
verify(request, times(2)).addHeader(headerCaptor.capture()); ArgumentCaptor<String> valueCaptor = ArgumentCaptor.forClass(String.class);
List<HTTPHeader> addedHeaders = headerCaptor.getAllValues(); verify(connection, times(2)).setRequestProperty(keyCaptor.capture(), valueCaptor.capture());
assertThat(addedHeaders.get(0).getName()).isEqualTo(CONTENT_TYPE); List<String> addedKeys = keyCaptor.getAllValues();
assertThat(addedHeaders.get(0).getValue()) assertThat(addedKeys).containsExactly(CONTENT_TYPE, CONTENT_LENGTH);
.isEqualTo( List<String> addedValues = valueCaptor.getAllValues();
"multipart/form-data; " assertThat(addedValues)
+ "boundary=\"------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\""); .containsExactly(
assertThat(addedHeaders.get(1).getName()).isEqualTo(CONTENT_LENGTH); "multipart/form-data;"
assertThat(addedHeaders.get(1).getValue()).isEqualTo("294"); + " boundary=\"------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"",
"294");
String payload = String payload =
"--------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n" "--------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n"
+ "Content-Disposition: form-data; name=\"lol\"; filename=\"cat\"\r\n" + "Content-Disposition: form-data; name=\"lol\"; filename=\"cat\"\r\n"
@ -84,18 +84,20 @@ class UrlFetchUtilsTest {
+ "\r\n" + "\r\n"
+ "The nice people at the store say hello. ヘ(◕。◕ヘ)\r\n" + "The nice people at the store say hello. ヘ(◕。◕ヘ)\r\n"
+ "--------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA--\r\n"; + "--------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA--\r\n";
verify(request).setPayload(payload.getBytes(UTF_8)); verify(connection).setDoOutput(true);
verifyNoMoreInteractions(request); verify(connection).getOutputStream();
assertThat(connectionOutputStream.toByteArray()).isEqualTo(payload.getBytes(UTF_8));
verifyNoMoreInteractions(connection);
} }
@Test @Test
void testSetPayloadMultipart_boundaryInPayload() { void testSetPayloadMultipart_boundaryInPayload() {
HTTPRequest request = mock(HTTPRequest.class); HttpsURLConnection connection = mock(HttpsURLConnection.class);
String payload = "I screamed------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHHH"; String payload = "I screamed------------------------------AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHHH";
IllegalStateException thrown = IllegalStateException thrown =
assertThrows( assertThrows(
IllegalStateException.class, IllegalStateException.class,
() -> setPayloadMultipart(request, "lol", "cat", CSV_UTF_8, payload, random)); () -> setPayloadMultipart(connection, "lol", "cat", CSV_UTF_8, payload, random));
assertThat(thrown) assertThat(thrown)
.hasMessageThat() .hasMessageThat()
.contains( .contains(

View file

@ -1,46 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.testing;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.common.collect.ImmutableList;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Map;
/**
* A fake {@link URLFetchService} that serves constructed {@link HTTPResponse} objects from
* a simple {@link Map} ({@link URL} to {@link HTTPResponse}) lookup.
*/
public class FakeURLFetchService extends ForwardingURLFetchService {
private Map<URL, HTTPResponse> backingMap;
public FakeURLFetchService(Map<URL, HTTPResponse> backingMap) {
this.backingMap = backingMap;
}
@Override
public HTTPResponse fetch(HTTPRequest request) {
URL requestURL = request.getURL();
if (backingMap.containsKey(requestURL)) {
return backingMap.get(requestURL);
} else {
return new HTTPResponse(HttpURLConnection.HTTP_NOT_FOUND, null, null, ImmutableList.of());
}
}
}

View file

@ -0,0 +1,46 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.testing;
import static org.mockito.Mockito.when;
import google.registry.request.UrlConnectionService;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
/** A fake {@link UrlConnectionService} with a mocked HTTP connection for testing. */
public class FakeUrlConnectionService implements UrlConnectionService {
private final HttpURLConnection mockConnection;
private final List<URL> connectedUrls;
public FakeUrlConnectionService(HttpURLConnection mockConnection) {
this(mockConnection, new ArrayList<>());
}
public FakeUrlConnectionService(HttpURLConnection mockConnection, List<URL> connectedUrls) {
this.mockConnection = mockConnection;
this.connectedUrls = connectedUrls;
}
@Override
public HttpURLConnection createConnection(URL url) {
connectedUrls.add(url);
when(mockConnection.getURL()).thenReturn(url);
return mockConnection;
}
}

View file

@ -1,49 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.testing;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.common.util.concurrent.Futures;
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.Future;
/**
* An implementation of the {@link URLFetchService} interface that forwards all requests through
* a synchronous fetch call.
*/
public abstract class ForwardingURLFetchService implements URLFetchService {
@Override
public HTTPResponse fetch(URL url) throws IOException {
return fetch(new HTTPRequest(url)); // Defaults to HTTPMethod.GET
}
@Override
public Future<HTTPResponse> fetchAsync(URL url) {
return fetchAsync(new HTTPRequest(url)); // Defaults to HTTPMethod.GET
}
@Override
public Future<HTTPResponse> fetchAsync(HTTPRequest request) {
try {
return Futures.immediateFuture(fetch(request));
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
}
}

View file

@ -19,21 +19,22 @@ import static com.google.common.net.HttpHeaders.CONTENT_TYPE;
import static com.google.common.net.HttpHeaders.LOCATION; import static com.google.common.net.HttpHeaders.LOCATION;
import static com.google.common.net.MediaType.FORM_DATA; import static com.google.common.net.MediaType.FORM_DATA;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadRegistrar; import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.newDomainBase; import static google.registry.testing.DatabaseHelper.newDomainBase;
import static google.registry.testing.DatabaseHelper.persistDomainAndEnqueueLordn; import static google.registry.testing.DatabaseHelper.persistDomainAndEnqueueLordn;
import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued; import static google.registry.testing.TaskQueueHelper.assertTasksEnqueued;
import static google.registry.util.UrlFetchUtils.getHeaderFirst;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED; import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.startsWith;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@ -43,10 +44,6 @@ import com.google.appengine.api.taskqueue.TaskHandle;
import com.google.appengine.api.taskqueue.TaskOptions; import com.google.appengine.api.taskqueue.TaskOptions;
import com.google.appengine.api.taskqueue.TaskOptions.Method; import com.google.appengine.api.taskqueue.TaskOptions.Method;
import com.google.appengine.api.taskqueue.TransientFailureException; import com.google.appengine.api.taskqueue.TransientFailureException;
import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.apphosting.api.DeadlineExceededException; import com.google.apphosting.api.DeadlineExceededException;
import com.google.common.base.VerifyException; import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
@ -57,11 +54,15 @@ import google.registry.model.tld.Registry;
import google.registry.testing.AppEngineExtension; import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeClock; import google.registry.testing.FakeClock;
import google.registry.testing.FakeSleeper; import google.registry.testing.FakeSleeper;
import google.registry.testing.FakeUrlConnectionService;
import google.registry.testing.InjectExtension; import google.registry.testing.InjectExtension;
import google.registry.testing.TaskQueueHelper.TaskMatcher; import google.registry.testing.TaskQueueHelper.TaskMatcher;
import google.registry.util.Retrier; import google.registry.util.Retrier;
import google.registry.util.TaskQueueUtils; import google.registry.util.TaskQueueUtils;
import google.registry.util.UrlFetchException; import google.registry.util.UrlConnectionException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.List; import java.util.List;
@ -69,17 +70,9 @@ import java.util.Optional;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
/** Unit tests for {@link NordnUploadAction}. */ /** Unit tests for {@link NordnUploadAction}. */
@ExtendWith(MockitoExtension.class)
class NordnUploadActionTest { class NordnUploadActionTest {
private static final String CLAIMS_CSV = private static final String CLAIMS_CSV =
@ -102,28 +95,29 @@ class NordnUploadActionTest {
@RegisterExtension public final InjectExtension inject = new InjectExtension(); @RegisterExtension public final InjectExtension inject = new InjectExtension();
@Mock private URLFetchService fetchService;
@Mock private HTTPResponse httpResponse;
@Captor private ArgumentCaptor<HTTPRequest> httpRequestCaptor;
private final FakeClock clock = new FakeClock(DateTime.parse("2010-05-01T10:11:12Z")); private final FakeClock clock = new FakeClock(DateTime.parse("2010-05-01T10:11:12Z"));
private final LordnRequestInitializer lordnRequestInitializer = private final LordnRequestInitializer lordnRequestInitializer =
new LordnRequestInitializer(Optional.of("attack")); new LordnRequestInitializer(Optional.of("attack"));
private final NordnUploadAction action = new NordnUploadAction(); private final NordnUploadAction action = new NordnUploadAction();
private final HttpURLConnection httpUrlConnection = mock(HttpURLConnection.class);
private final ByteArrayOutputStream connectionOutputStream = new ByteArrayOutputStream();
private final FakeUrlConnectionService urlConnectionService =
new FakeUrlConnectionService(httpUrlConnection);
@BeforeEach @BeforeEach
void beforeEach() throws Exception { void beforeEach() throws Exception {
inject.setStaticField(Ofy.class, "clock", clock); inject.setStaticField(Ofy.class, "clock", clock);
when(fetchService.fetch(any(HTTPRequest.class))).thenReturn(httpResponse); when(httpUrlConnection.getInputStream())
when(httpResponse.getContent()).thenReturn("Success".getBytes(US_ASCII)); .thenReturn(new ByteArrayInputStream("Success".getBytes(UTF_8)));
when(httpResponse.getResponseCode()).thenReturn(SC_ACCEPTED); when(httpUrlConnection.getResponseCode()).thenReturn(SC_ACCEPTED);
when(httpResponse.getHeadersUncombined()) when(httpUrlConnection.getHeaderField(LOCATION)).thenReturn("http://trololol");
.thenReturn(ImmutableList.of(new HTTPHeader(LOCATION, "http://trololol"))); when(httpUrlConnection.getOutputStream()).thenReturn(connectionOutputStream);
persistResource(loadRegistrar("TheRegistrar").asBuilder().setIanaIdentifier(99999L).build()); persistResource(loadRegistrar("TheRegistrar").asBuilder().setIanaIdentifier(99999L).build());
createTld("tld"); createTld("tld");
persistResource(Registry.get("tld").asBuilder().setLordnUsername("lolcat").build()); persistResource(Registry.get("tld").asBuilder().setLordnUsername("lolcat").build());
action.clock = clock; action.clock = clock;
action.fetchService = fetchService; action.urlConnectionService = urlConnectionService;
action.lordnRequestInitializer = lordnRequestInitializer; action.lordnRequestInitializer = lordnRequestInitializer;
action.phase = "claims"; action.phase = "claims";
action.taskQueueUtils = new TaskQueueUtils(new Retrier(new FakeSleeper(clock), 3)); action.taskQueueUtils = new TaskQueueUtils(new Retrier(new FakeSleeper(clock), 3));
@ -133,7 +127,6 @@ class NordnUploadActionTest {
action.retrier = new Retrier(new FakeSleeper(clock), 3); action.retrier = new Retrier(new FakeSleeper(clock), 3);
} }
@MockitoSettings(strictness = Strictness.LENIENT)
@Test @Test
void test_convertTasksToCsv() { void test_convertTasksToCsv() {
List<TaskHandle> tasks = List<TaskHandle> tasks =
@ -145,7 +138,6 @@ class NordnUploadActionTest {
.isEqualTo("1,2010-05-01T10:11:12.000Z,3\ncol1,col2\ncsvLine1\ncsvLine2\nending\n"); .isEqualTo("1,2010-05-01T10:11:12.000Z,3\ncol1,col2\ncsvLine1\ncsvLine2\nending\n");
} }
@MockitoSettings(strictness = Strictness.LENIENT)
@Test @Test
void test_convertTasksToCsv_dedupesDuplicates() { void test_convertTasksToCsv_dedupesDuplicates() {
List<TaskHandle> tasks = List<TaskHandle> tasks =
@ -158,14 +150,12 @@ class NordnUploadActionTest {
.isEqualTo("1,2010-05-01T10:11:12.000Z,3\ncol1,col2\ncsvLine1\ncsvLine2\nending\n"); .isEqualTo("1,2010-05-01T10:11:12.000Z,3\ncol1,col2\ncsvLine1\ncsvLine2\nending\n");
} }
@MockitoSettings(strictness = Strictness.LENIENT)
@Test @Test
void test_convertTasksToCsv_doesntFailOnEmptyTasks() { void test_convertTasksToCsv_doesntFailOnEmptyTasks() {
assertThat(NordnUploadAction.convertTasksToCsv(ImmutableList.of(), clock.nowUtc(), "col1,col2")) assertThat(NordnUploadAction.convertTasksToCsv(ImmutableList.of(), clock.nowUtc(), "col1,col2"))
.isEqualTo("1,2010-05-01T10:11:12.000Z,0\ncol1,col2\n"); .isEqualTo("1,2010-05-01T10:11:12.000Z,0\ncol1,col2\n");
} }
@MockitoSettings(strictness = Strictness.LENIENT)
@Test @Test
void test_convertTasksToCsv_throwsNpeOnNullTasks() { void test_convertTasksToCsv_throwsNpeOnNullTasks() {
assertThrows( assertThrows(
@ -173,7 +163,6 @@ class NordnUploadActionTest {
() -> NordnUploadAction.convertTasksToCsv(null, clock.nowUtc(), "header")); () -> NordnUploadAction.convertTasksToCsv(null, clock.nowUtc(), "header"));
} }
@MockitoSettings(strictness = Strictness.LENIENT)
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@Test @Test
void test_loadAllTasks_retryLogic_thirdTrysTheCharm() { void test_loadAllTasks_retryLogic_thirdTrysTheCharm() {
@ -186,7 +175,6 @@ class NordnUploadActionTest {
assertThat(action.loadAllTasks(queue, "tld")).containsExactly(task); assertThat(action.loadAllTasks(queue, "tld")).containsExactly(task);
} }
@MockitoSettings(strictness = Strictness.LENIENT)
@Test @Test
void test_loadAllTasks_retryLogic_allFailures() { void test_loadAllTasks_retryLogic_allFailures() {
Queue queue = mock(Queue.class); Queue queue = mock(Queue.class);
@ -201,47 +189,47 @@ class NordnUploadActionTest {
void testRun_claimsMode_appendsTldAndClaimsToRequestUrl() throws Exception { void testRun_claimsMode_appendsTldAndClaimsToRequestUrl() throws Exception {
persistClaimsModeDomain(); persistClaimsModeDomain();
action.run(); action.run();
assertThat(getCapturedHttpRequest().getURL()) assertThat(httpUrlConnection.getURL()).isEqualTo(new URL("http://127.0.0.1/LORDN/tld/claims"));
.isEqualTo(new URL("http://127.0.0.1/LORDN/tld/claims"));
} }
@Test @Test
void testRun_sunriseMode_appendsTldAndClaimsToRequestUrl() throws Exception { void testRun_sunriseMode_appendsTldAndClaimsToRequestUrl() throws Exception {
persistSunriseModeDomain(); persistSunriseModeDomain();
action.run(); action.run();
assertThat(getCapturedHttpRequest().getURL()) assertThat(httpUrlConnection.getURL()).isEqualTo(new URL("http://127.0.0.1/LORDN/tld/sunrise"));
.isEqualTo(new URL("http://127.0.0.1/LORDN/tld/sunrise"));
} }
@Test @Test
void testRun_usesMultipartContentType() throws Exception { void testRun_usesMultipartContentType() throws Exception {
persistClaimsModeDomain(); persistClaimsModeDomain();
action.run(); action.run();
assertThat(getHeaderFirst(getCapturedHttpRequest(), CONTENT_TYPE).get()) verify(httpUrlConnection)
.startsWith("multipart/form-data; boundary="); .setRequestProperty(eq(CONTENT_TYPE), startsWith("multipart/form-data; boundary="));
verify(httpUrlConnection).setRequestMethod("POST");
} }
@Test @Test
void testRun_hasPassword_setsAuthorizationHeader() throws Exception { void testRun_hasPassword_setsAuthorizationHeader() {
persistClaimsModeDomain(); persistClaimsModeDomain();
action.run(); action.run();
assertThat(getHeaderFirst(getCapturedHttpRequest(), AUTHORIZATION)) verify(httpUrlConnection)
.hasValue("Basic bG9sY2F0OmF0dGFjaw=="); // echo -n lolcat:attack | base64 .setRequestProperty(
AUTHORIZATION, "Basic bG9sY2F0OmF0dGFjaw=="); // echo -n lolcat:attack | base64
} }
@Test @Test
void testRun_noPassword_doesntSendAuthorizationHeader() throws Exception { void testRun_noPassword_doesntSendAuthorizationHeader() {
action.lordnRequestInitializer = new LordnRequestInitializer(Optional.empty()); action.lordnRequestInitializer = new LordnRequestInitializer(Optional.empty());
persistClaimsModeDomain(); persistClaimsModeDomain();
action.run(); action.run();
assertThat(getHeaderFirst(getCapturedHttpRequest(), AUTHORIZATION)).isEmpty(); verify(httpUrlConnection, times(0)).setRequestProperty(eq(AUTHORIZATION), anyString());
} }
@Test @Test
void testRun_claimsMode_payloadMatchesClaimsCsv() throws Exception { void testRun_claimsMode_payloadMatchesClaimsCsv() {
persistClaimsModeDomain(); persistClaimsModeDomain();
action.run(); action.run();
assertThat(new String(getCapturedHttpRequest().getPayload(), UTF_8)).contains(CLAIMS_CSV); assertThat(new String(connectionOutputStream.toByteArray(), UTF_8)).contains(CLAIMS_CSV);
} }
@Test @Test
@ -257,19 +245,19 @@ class NordnUploadActionTest {
} }
@Test @Test
void testRun_sunriseMode_payloadMatchesSunriseCsv() throws Exception { void testRun_sunriseMode_payloadMatchesSunriseCsv() {
persistSunriseModeDomain(); persistSunriseModeDomain();
action.run(); action.run();
assertThat(new String(getCapturedHttpRequest().getPayload(), UTF_8)).contains(SUNRISE_CSV); assertThat(new String(connectionOutputStream.toByteArray(), UTF_8)).contains(SUNRISE_CSV);
} }
@Test @Test
void test_noResponseContent_stillWorksNormally() throws Exception { void test_noResponseContent_stillWorksNormally() throws Exception {
// Returning null only affects logging. // Returning null only affects logging.
when(httpResponse.getContent()).thenReturn(null); when(httpUrlConnection.getInputStream()).thenReturn(new ByteArrayInputStream(new byte[] {}));
persistSunriseModeDomain(); persistSunriseModeDomain();
action.run(); action.run();
assertThat(new String(getCapturedHttpRequest().getPayload(), UTF_8)).contains(SUNRISE_CSV); assertThat(new String(connectionOutputStream.toByteArray(), UTF_8)).contains(SUNRISE_CSV);
} }
@Test @Test
@ -284,7 +272,6 @@ class NordnUploadActionTest {
.header(CONTENT_TYPE, FORM_DATA.toString())); .header(CONTENT_TYPE, FORM_DATA.toString()));
} }
@MockitoSettings(strictness = Strictness.LENIENT)
@Test @Test
void testFailure_nullRegistryUser() { void testFailure_nullRegistryUser() {
persistClaimsModeDomain(); persistClaimsModeDomain();
@ -293,17 +280,11 @@ class NordnUploadActionTest {
assertThat(thrown).hasMessageThat().contains("lordnUsername is not set for tld."); assertThat(thrown).hasMessageThat().contains("lordnUsername is not set for tld.");
} }
@MockitoSettings(strictness = Strictness.LENIENT)
@Test @Test
void testFetchFailure() { void testFetchFailure() throws Exception {
persistClaimsModeDomain(); persistClaimsModeDomain();
when(httpResponse.getResponseCode()).thenReturn(SC_INTERNAL_SERVER_ERROR); when(httpUrlConnection.getResponseCode()).thenReturn(SC_INTERNAL_SERVER_ERROR);
assertThrows(UrlFetchException.class, action::run); assertThrows(UrlConnectionException.class, action::run);
}
private HTTPRequest getCapturedHttpRequest() throws Exception {
verify(fetchService).fetch(httpRequestCaptor.capture());
return httpRequestCaptor.getAllValues().get(0);
} }
private void persistClaimsModeDomain() { private void persistClaimsModeDomain() {

View file

@ -16,39 +16,34 @@ package google.registry.tmch;
import static com.google.common.net.HttpHeaders.AUTHORIZATION; import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.testing.DatabaseHelper.createTld; import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistResource; import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.UrlFetchUtils.getHeaderFirst;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_OK;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import google.registry.model.tld.Registry; import google.registry.model.tld.Registry;
import google.registry.request.HttpException.ConflictException; import google.registry.request.HttpException.ConflictException;
import google.registry.testing.AppEngineExtension; import google.registry.testing.AppEngineExtension;
import google.registry.testing.FakeResponse; import google.registry.testing.FakeResponse;
import google.registry.testing.FakeUrlConnectionService;
import java.io.ByteArrayInputStream;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.util.Optional; import java.util.Optional;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
/** Unit tests for {@link NordnVerifyAction}. */ /** Unit tests for {@link NordnVerifyAction}. */
@ExtendWith(MockitoExtension.class)
class NordnVerifyActionTest { class NordnVerifyActionTest {
private static final String LOG_ACCEPTED = private static final String LOG_ACCEPTED =
@ -81,84 +76,83 @@ class NordnVerifyActionTest {
public final AppEngineExtension appEngine = public final AppEngineExtension appEngine =
AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build(); AppEngineExtension.builder().withDatastoreAndCloudSql().withTaskQueue().build();
@Mock private URLFetchService fetchService;
@Mock private HTTPResponse httpResponse;
@Captor private ArgumentCaptor<HTTPRequest> httpRequestCaptor;
private final FakeResponse response = new FakeResponse(); private final FakeResponse response = new FakeResponse();
private final LordnRequestInitializer lordnRequestInitializer = private final LordnRequestInitializer lordnRequestInitializer =
new LordnRequestInitializer(Optional.of("attack")); new LordnRequestInitializer(Optional.of("attack"));
private final NordnVerifyAction action = new NordnVerifyAction(); private final NordnVerifyAction action = new NordnVerifyAction();
private final HttpURLConnection httpUrlConnection = mock(HttpURLConnection.class);
private final FakeUrlConnectionService urlConnectionService =
new FakeUrlConnectionService(httpUrlConnection);
@BeforeEach @BeforeEach
void beforeEach() throws Exception { void beforeEach() throws Exception {
when(httpResponse.getResponseCode()).thenReturn(SC_OK);
when(httpResponse.getContent()).thenReturn(LOG_ACCEPTED.getBytes(UTF_8));
when(fetchService.fetch(any(HTTPRequest.class))).thenReturn(httpResponse);
createTld("gtld"); createTld("gtld");
persistResource(Registry.get("gtld").asBuilder().setLordnUsername("lolcat").build()); persistResource(Registry.get("gtld").asBuilder().setLordnUsername("lolcat").build());
action.tld = "gtld"; action.tld = "gtld";
action.fetchService = fetchService; action.urlConnectionService = urlConnectionService;
when(httpUrlConnection.getResponseCode()).thenReturn(SC_OK);
when(httpUrlConnection.getInputStream())
.thenReturn(new ByteArrayInputStream(LOG_ACCEPTED.getBytes(UTF_8)));
action.lordnRequestInitializer = lordnRequestInitializer; action.lordnRequestInitializer = lordnRequestInitializer;
action.response = response; action.response = response;
action.url = new URL("http://127.0.0.1/blobio"); action.url = new URL("http://127.0.0.1/blobio");
} }
private HTTPRequest getCapturedHttpRequest() throws Exception {
verify(fetchService).fetch(httpRequestCaptor.capture());
return httpRequestCaptor.getAllValues().get(0);
}
@Test @Test
void testSuccess_sendHttpRequest_urlIsCorrect() throws Exception { void testSuccess_sendHttpRequest_urlIsCorrect() throws Exception {
action.run(); action.run();
assertThat(getCapturedHttpRequest().getURL()).isEqualTo(new URL("http://127.0.0.1/blobio")); assertThat(httpUrlConnection.getURL()).isEqualTo(new URL("http://127.0.0.1/blobio"));
} }
@Test @Test
void testSuccess_hasLordnPassword_sendsAuthorizationHeader() throws Exception { void testSuccess_hasLordnPassword_sendsAuthorizationHeader() {
action.run(); action.run();
assertThat(getHeaderFirst(getCapturedHttpRequest(), AUTHORIZATION)) verify(httpUrlConnection)
.hasValue("Basic bG9sY2F0OmF0dGFjaw=="); // echo -n lolcat:attack | base64 .setRequestProperty(
AUTHORIZATION, "Basic bG9sY2F0OmF0dGFjaw=="); // echo -n lolcat:attack | base64
} }
@Test @Test
void testSuccess_noLordnPassword_doesntSetAuthorizationHeader() throws Exception { void testSuccess_noLordnPassword_doesntSetAuthorizationHeader() {
action.lordnRequestInitializer = new LordnRequestInitializer(Optional.empty()); action.lordnRequestInitializer = new LordnRequestInitializer(Optional.empty());
action.run(); action.run();
assertThat(getHeaderFirst(getCapturedHttpRequest(), AUTHORIZATION)).isEmpty(); verify(httpUrlConnection, times(0)).setRequestProperty(eq(AUTHORIZATION), anyString());
} }
@Test @Test
void successVerifyRejected() throws Exception { void successVerifyRejected() throws Exception {
when(httpResponse.getContent()).thenReturn(LOG_REJECTED.getBytes(UTF_8)); when(httpUrlConnection.getInputStream())
.thenReturn(new ByteArrayInputStream(LOG_REJECTED.getBytes(UTF_8)));
LordnLog lastLog = action.verify(); LordnLog lastLog = action.verify();
assertThat(lastLog.getStatus()).isEqualTo(LordnLog.Status.REJECTED); assertThat(lastLog.getStatus()).isEqualTo(LordnLog.Status.REJECTED);
} }
@Test @Test
void successVerifyWarnings() throws Exception { void successVerifyWarnings() throws Exception {
when(httpResponse.getContent()).thenReturn(LOG_WARNINGS.getBytes(UTF_8)); when(httpUrlConnection.getInputStream())
.thenReturn(new ByteArrayInputStream(LOG_WARNINGS.getBytes(UTF_8)));
LordnLog lastLog = action.verify(); LordnLog lastLog = action.verify();
assertThat(lastLog.hasWarnings()).isTrue(); assertThat(lastLog.hasWarnings()).isTrue();
} }
@Test @Test
void successVerifyErrors() throws Exception { void successVerifyErrors() throws Exception {
when(httpResponse.getContent()).thenReturn(LOG_ERRORS.getBytes(UTF_8)); when(httpUrlConnection.getInputStream())
.thenReturn(new ByteArrayInputStream(LOG_ERRORS.getBytes(UTF_8)));
LordnLog lastLog = action.verify(); LordnLog lastLog = action.verify();
assertThat(lastLog.hasWarnings()).isTrue(); assertThat(lastLog.hasWarnings()).isTrue();
} }
@Test @Test
void failureVerifyUnauthorized() { void failureVerifyUnauthorized() throws Exception {
when(httpResponse.getResponseCode()).thenReturn(SC_UNAUTHORIZED); when(httpUrlConnection.getResponseCode()).thenReturn(SC_UNAUTHORIZED);
assertThrows(Exception.class, action::run); assertThrows(Exception.class, action::run);
} }
@Test @Test
void failureVerifyNotReady() { void failureVerifyNotReady() throws Exception {
when(httpResponse.getResponseCode()).thenReturn(SC_NO_CONTENT); when(httpUrlConnection.getResponseCode()).thenReturn(SC_NO_CONTENT);
ConflictException thrown = assertThrows(ConflictException.class, action::run); ConflictException thrown = assertThrows(ConflictException.class, action::run);
assertThat(thrown).hasMessageThat().contains("Not ready"); assertThat(thrown).hasMessageThat().contains("Not ready");
} }

View file

@ -15,21 +15,19 @@
package google.registry.tmch; package google.registry.tmch;
import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import google.registry.testing.AppEngineExtension; import google.registry.testing.AppEngineExtension;
import google.registry.testing.BouncyCastleProviderExtension; import google.registry.testing.BouncyCastleProviderExtension;
import google.registry.testing.FakeClock; import google.registry.testing.FakeClock;
import google.registry.testing.FakeUrlConnectionService;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
/** Common code for unit tests of classes that extend {@link Marksdb}. */ /** Common code for unit tests of classes that extend {@link Marksdb}. */
@ -46,19 +44,19 @@ abstract class TmchActionTestCase {
@RegisterExtension @RegisterExtension
public final BouncyCastleProviderExtension bouncy = new BouncyCastleProviderExtension(); public final BouncyCastleProviderExtension bouncy = new BouncyCastleProviderExtension();
@Mock URLFetchService fetchService;
@Mock HTTPResponse httpResponse;
@Captor ArgumentCaptor<HTTPRequest> httpRequest;
final FakeClock clock = new FakeClock(); final FakeClock clock = new FakeClock();
final Marksdb marksdb = new Marksdb(); final Marksdb marksdb = new Marksdb();
protected final HttpURLConnection httpUrlConnection = mock(HttpURLConnection.class);
protected final ArrayList<URL> connectedUrls = new ArrayList<>();
protected FakeUrlConnectionService urlConnectionService =
new FakeUrlConnectionService(httpUrlConnection, connectedUrls);
@BeforeEach @BeforeEach
public void beforeEachTmchActionTestCase() throws Exception { public void beforeEachTmchActionTestCase() throws Exception {
marksdb.fetchService = fetchService;
marksdb.tmchMarksdbUrl = MARKSDB_URL; marksdb.tmchMarksdbUrl = MARKSDB_URL;
marksdb.marksdbPublicKey = TmchData.loadPublicKey(TmchTestData.loadBytes("pubkey")); marksdb.marksdbPublicKey = TmchData.loadPublicKey(TmchTestData.loadBytes("pubkey"));
when(fetchService.fetch(any(HTTPRequest.class))).thenReturn(httpResponse); marksdb.urlConnectionService = urlConnectionService;
when(httpResponse.getResponseCode()).thenReturn(SC_OK); when(httpUrlConnection.getResponseCode()).thenReturn(SC_OK);
} }
} }

View file

@ -22,6 +22,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import google.registry.config.RegistryConfig.ConfigModule.TmchCaMode; import google.registry.config.RegistryConfig.ConfigModule.TmchCaMode;
import java.io.ByteArrayInputStream;
import java.net.MalformedURLException; import java.net.MalformedURLException;
import java.net.URL; import java.net.URL;
import java.security.SignatureException; import java.security.SignatureException;
@ -44,19 +45,22 @@ class TmchCrlActionTest extends TmchActionTestCase {
@Test @Test
void testSuccess() throws Exception { void testSuccess() throws Exception {
clock.setTo(DateTime.parse("2013-07-24TZ")); clock.setTo(DateTime.parse("2013-07-24TZ"));
when(httpResponse.getContent()).thenReturn( when(httpUrlConnection.getInputStream())
readResourceBytes(TmchCertificateAuthority.class, "icann-tmch.crl").read()); .thenReturn(
new ByteArrayInputStream(
readResourceBytes(TmchCertificateAuthority.class, "icann-tmch.crl").read()));
newTmchCrlAction(TmchCaMode.PRODUCTION).run(); newTmchCrlAction(TmchCaMode.PRODUCTION).run();
verify(httpResponse).getContent(); verify(httpUrlConnection).getInputStream();
verify(fetchService).fetch(httpRequest.capture()); assertThat(connectedUrls).containsExactly(new URL("http://sloth.lol/tmch.crl"));
assertThat(httpRequest.getValue().getURL().toString()).isEqualTo("http://sloth.lol/tmch.crl");
} }
@Test @Test
void testFailure_crlTooOld() throws Exception { void testFailure_crlTooOld() throws Exception {
clock.setTo(DateTime.parse("2020-01-01TZ")); clock.setTo(DateTime.parse("2020-01-01TZ"));
when(httpResponse.getContent()) when(httpUrlConnection.getInputStream())
.thenReturn(loadBytes(TmchCrlActionTest.class, "icann-tmch-pilot-old.crl").read()); .thenReturn(
new ByteArrayInputStream(
loadBytes(TmchCrlActionTest.class, "icann-tmch-pilot-old.crl").read()));
TmchCrlAction action = newTmchCrlAction(TmchCaMode.PILOT); TmchCrlAction action = newTmchCrlAction(TmchCaMode.PILOT);
Exception e = assertThrows(Exception.class, action::run); Exception e = assertThrows(Exception.class, action::run);
assertThat(e).hasCauseThat().isInstanceOf(CRLException.class); assertThat(e).hasCauseThat().isInstanceOf(CRLException.class);
@ -69,8 +73,10 @@ class TmchCrlActionTest extends TmchActionTestCase {
@Test @Test
void testFailure_crlNotSignedByRoot() throws Exception { void testFailure_crlNotSignedByRoot() throws Exception {
clock.setTo(DateTime.parse("2013-07-24TZ")); clock.setTo(DateTime.parse("2013-07-24TZ"));
when(httpResponse.getContent()) when(httpUrlConnection.getInputStream())
.thenReturn(readResourceBytes(TmchCertificateAuthority.class, "icann-tmch.crl").read()); .thenReturn(
new ByteArrayInputStream(
readResourceBytes(TmchCertificateAuthority.class, "icann-tmch.crl").read()));
Exception e = assertThrows(Exception.class, newTmchCrlAction(TmchCaMode.PILOT)::run); Exception e = assertThrows(Exception.class, newTmchCrlAction(TmchCaMode.PILOT)::run);
assertThat(e).hasCauseThat().isInstanceOf(SignatureException.class); assertThat(e).hasCauseThat().isInstanceOf(SignatureException.class);
assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("Signature does not match."); assertThat(e).hasCauseThat().hasMessageThat().isEqualTo("Signature does not match.");
@ -79,8 +85,10 @@ class TmchCrlActionTest extends TmchActionTestCase {
@Test @Test
void testFailure_crlNotYetValid() throws Exception { void testFailure_crlNotYetValid() throws Exception {
clock.setTo(DateTime.parse("1984-01-01TZ")); clock.setTo(DateTime.parse("1984-01-01TZ"));
when(httpResponse.getContent()).thenReturn( when(httpUrlConnection.getInputStream())
readResourceBytes(TmchCertificateAuthority.class, "icann-tmch-pilot.crl").read()); .thenReturn(
new ByteArrayInputStream(
readResourceBytes(TmchCertificateAuthority.class, "icann-tmch-pilot.crl").read()));
Exception e = assertThrows(Exception.class, newTmchCrlAction(TmchCaMode.PILOT)::run); Exception e = assertThrows(Exception.class, newTmchCrlAction(TmchCaMode.PILOT)::run);
assertThat(e).hasCauseThat().isInstanceOf(CertificateNotYetValidException.class); assertThat(e).hasCauseThat().isInstanceOf(CertificateNotYetValidException.class);
} }

View file

@ -14,6 +14,7 @@
package google.registry.tmch; package google.registry.tmch;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat; import static com.google.common.truth.Truth8.assertThat;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@ -22,6 +23,8 @@ import static org.mockito.Mockito.when;
import google.registry.model.tmch.ClaimsList; import google.registry.model.tmch.ClaimsList;
import google.registry.model.tmch.ClaimsListDao; import google.registry.model.tmch.ClaimsListDao;
import java.io.ByteArrayInputStream;
import java.net.URL;
import java.util.Optional; import java.util.Optional;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -39,15 +42,13 @@ class TmchDnlActionTest extends TmchActionTestCase {
@Test @Test
void testDnl() throws Exception { void testDnl() throws Exception {
assertThat(ClaimsListDao.get().getClaimKey("xn----7sbejwbn3axu3d")).isEmpty(); assertThat(ClaimsListDao.get().getClaimKey("xn----7sbejwbn3axu3d")).isEmpty();
when(httpResponse.getContent()) when(httpUrlConnection.getInputStream())
.thenReturn(TmchTestData.loadBytes("dnl-latest.csv").read()) .thenReturn(new ByteArrayInputStream(TmchTestData.loadBytes("dnl-latest.csv").read()))
.thenReturn(TmchTestData.loadBytes("dnl-latest.sig").read()); .thenReturn(new ByteArrayInputStream(TmchTestData.loadBytes("dnl-latest.sig").read()));
newTmchDnlAction().run(); newTmchDnlAction().run();
verify(fetchService, times(2)).fetch(httpRequest.capture()); verify(httpUrlConnection, times(2)).getInputStream();
assertThat(httpRequest.getAllValues().get(0).getURL().toString()) assertThat(connectedUrls.stream().map(URL::toString).collect(toImmutableList()))
.isEqualTo(MARKSDB_URL + "/dnl/dnl-latest.csv"); .containsExactly(MARKSDB_URL + "/dnl/dnl-latest.csv", MARKSDB_URL + "/dnl/dnl-latest.sig");
assertThat(httpRequest.getAllValues().get(1).getURL().toString())
.isEqualTo(MARKSDB_URL + "/dnl/dnl-latest.sig");
// Make sure the contents of testdata/dnl-latest.csv got inserted into the database. // Make sure the contents of testdata/dnl-latest.csv got inserted into the database.
ClaimsList claimsList = ClaimsListDao.get(); ClaimsList claimsList = ClaimsListDao.get();

View file

@ -14,6 +14,7 @@
package google.registry.tmch; package google.registry.tmch;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static google.registry.tmch.TmchTestData.loadBytes; import static google.registry.tmch.TmchTestData.loadBytes;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
@ -21,6 +22,8 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import google.registry.model.smd.SignedMarkRevocationList; import google.registry.model.smd.SignedMarkRevocationList;
import java.io.ByteArrayInputStream;
import java.net.URL;
import java.util.Optional; import java.util.Optional;
import org.joda.time.DateTime; import org.joda.time.DateTime;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -42,15 +45,14 @@ class TmchSmdrlActionTest extends TmchActionTestCase {
SignedMarkRevocationList smdrl = SignedMarkRevocationList.get(); SignedMarkRevocationList smdrl = SignedMarkRevocationList.get();
assertThat(smdrl.isSmdRevoked("0000001681375789102250-65535", now)).isFalse(); assertThat(smdrl.isSmdRevoked("0000001681375789102250-65535", now)).isFalse();
assertThat(smdrl.isSmdRevoked("0000001681375789102250-65536", now)).isFalse(); assertThat(smdrl.isSmdRevoked("0000001681375789102250-65536", now)).isFalse();
when(httpResponse.getContent()) when(httpUrlConnection.getInputStream())
.thenReturn(loadBytes("smdrl-latest.csv").read()) .thenReturn(new ByteArrayInputStream(loadBytes("smdrl-latest.csv").read()))
.thenReturn(loadBytes("smdrl-latest.sig").read()); .thenReturn(new ByteArrayInputStream(loadBytes("smdrl-latest.sig").read()));
newTmchSmdrlAction().run(); newTmchSmdrlAction().run();
verify(fetchService, times(2)).fetch(httpRequest.capture()); verify(httpUrlConnection, times(2)).getInputStream();
assertThat(httpRequest.getAllValues().get(0).getURL().toString()) assertThat(connectedUrls.stream().map(URL::toString).collect(toImmutableList()))
.isEqualTo(MARKSDB_URL + "/smdrl/smdrl-latest.csv"); .containsExactly(
assertThat(httpRequest.getAllValues().get(1).getURL().toString()) MARKSDB_URL + "/smdrl/smdrl-latest.csv", MARKSDB_URL + "/smdrl/smdrl-latest.sig");
.isEqualTo(MARKSDB_URL + "/smdrl/smdrl-latest.sig");
smdrl = SignedMarkRevocationList.get(); smdrl = SignedMarkRevocationList.get();
assertThat(smdrl.isSmdRevoked("0000001681375789102250-65535", now)).isTrue(); assertThat(smdrl.isSmdRevoked("0000001681375789102250-65535", now)).isTrue();
assertThat(smdrl.isSmdRevoked("0000001681375789102250-65536", now)).isFalse(); assertThat(smdrl.isSmdRevoked("0000001681375789102250-65536", now)).isFalse();

View file

@ -0,0 +1,71 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.util;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.io.ByteStreams;
import java.io.IOException;
import java.net.HttpURLConnection;
/**
* Used when HTTP requests return a bad response, with troubleshooting info.
*
* <p>This class displays lots of helpful troubleshooting information.
*/
public class UrlConnectionException extends RuntimeException {
private final HttpURLConnection connection;
public UrlConnectionException(String message, HttpURLConnection connection) {
super(message);
this.connection = connection;
}
@Override
public String getMessage() {
byte[] resultContent;
int responseCode;
try {
resultContent = ByteStreams.toByteArray(connection.getInputStream());
responseCode = connection.getResponseCode();
} catch (IOException e) {
resultContent = new byte[] {};
responseCode = 0;
}
StringBuilder result =
new StringBuilder(2048 + resultContent.length)
.append(
String.format(
"%s: %s (HTTP Status %d)\nX-Fetch-URL: %s\n",
getClass().getSimpleName(),
super.getMessage(),
responseCode,
connection.getURL().toString()));
connection
.getRequestProperties()
.forEach(
(key, value) -> {
result.append(key);
result.append(": ");
result.append(value);
result.append('\n');
});
result.append(">>>\n");
result.append(new String(resultContent, UTF_8));
result.append("\n<<<");
return result.toString();
}
}

View file

@ -1,63 +0,0 @@
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.util;
import static com.google.common.base.Preconditions.checkNotNull;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
/**
* Exception for when App Engine HTTP requests return a bad response.
*
* <p>This class displays lots of helpful troubleshooting information.
*/
public class UrlFetchException extends RuntimeException {
private final HTTPRequest req;
private final HTTPResponse rsp;
public UrlFetchException(String message, HTTPRequest req, HTTPResponse rsp) {
super(message);
this.req = checkNotNull(req, "req");
this.rsp = checkNotNull(rsp, "rsp");
}
@Override
public String getMessage() {
StringBuilder res =
new StringBuilder(2048 + rsp.getContent().length)
.append(
String.format(
"%s: %s (HTTP Status %d)\nX-Fetch-URL: %s\nX-Final-URL: %s\n",
getClass().getSimpleName(),
super.getMessage(),
rsp.getResponseCode(),
req.getURL().toString(),
rsp.getFinalUrl()));
for (HTTPHeader header : rsp.getHeadersUncombined()) {
res.append(header.getName());
res.append(": ");
res.append(header.getValue());
res.append('\n');
}
res.append(">>>\n");
res.append(new String(rsp.getContent(), UTF_8));
res.append("\n<<<");
return res.toString();
}
}