mirror of
https://github.com/google/nomulus
synced 2026-05-25 01:01:57 +00:00
Compare commits
7 Commits
nomulus-20
...
nomulus-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cef2dd8b5 | ||
|
|
62b2585220 | ||
|
|
8692fe35db | ||
|
|
18614ba11e | ||
|
|
427f6db820 | ||
|
|
5aa40b2208 | ||
|
|
95c89bc856 |
@@ -18,11 +18,12 @@ import static com.google.common.base.Throwables.getRootCause;
|
||||
import static google.registry.request.Action.Method.POST;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static java.util.stream.Collectors.joining;
|
||||
|
||||
import com.google.cloud.storage.BlobId;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.ByteStreams;
|
||||
@@ -41,7 +42,6 @@ import jakarta.inject.Inject;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** Copy all registrar detail reports in a given bucket's subdirectory from GCS to Drive. */
|
||||
@Action(
|
||||
@@ -98,7 +98,8 @@ public final class CopyDetailReportsAction implements Runnable {
|
||||
response.setPayload(String.format("Failure, encountered %s", e.getMessage()));
|
||||
return;
|
||||
}
|
||||
ImmutableMap.Builder<String, Throwable> copyErrorsBuilder = new ImmutableMap.Builder<>();
|
||||
ImmutableMultimap.Builder<String, Throwable> copyErrorsBuilder =
|
||||
new ImmutableMultimap.Builder<>();
|
||||
for (String detailReportName : detailReportObjectNames) {
|
||||
// The standard report format is "invoice_details_yyyy-MM_registrarId_tld.csv
|
||||
// TODO(larryruili): Determine a safer way of enforcing this.
|
||||
@@ -145,17 +146,18 @@ public final class CopyDetailReportsAction implements Runnable {
|
||||
response.setStatus(SC_OK);
|
||||
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||
StringBuilder payload = new StringBuilder().append("Copied detail reports.\n");
|
||||
ImmutableMap<String, Throwable> copyErrors = copyErrorsBuilder.build();
|
||||
ImmutableMultimap<String, Throwable> copyErrors = copyErrorsBuilder.build();
|
||||
if (!copyErrors.isEmpty()) {
|
||||
payload.append("The following errors were encountered:\n");
|
||||
payload.append(
|
||||
copyErrors.entrySet().stream()
|
||||
.map(
|
||||
entrySet ->
|
||||
String.format(
|
||||
"Registrar: %s\nError: %s\n",
|
||||
entrySet.getKey(), entrySet.getValue().getMessage()))
|
||||
.collect(Collectors.joining()));
|
||||
for (var registrarId : copyErrors.keySet()) {
|
||||
payload.append(
|
||||
String.format(
|
||||
"Registrar: %s\nError: %s\n",
|
||||
registrarId,
|
||||
copyErrors.get(registrarId).stream()
|
||||
.map(Throwable::getMessage)
|
||||
.collect(joining("\n\t"))));
|
||||
}
|
||||
}
|
||||
response.setPayload(payload.toString());
|
||||
emailUtils.sendAlertEmail(payload.toString());
|
||||
|
||||
@@ -120,7 +120,7 @@ public class SecurityAction extends ConsoleApiAction {
|
||||
}
|
||||
}
|
||||
} catch (InsecureCertificateException e) {
|
||||
setFailedResponse("Invalid certificate in parameter", SC_BAD_REQUEST);
|
||||
setFailedResponse(e.getMessage(), SC_BAD_REQUEST);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -178,21 +178,56 @@ class CopyDetailReportsActionTest {
|
||||
verify(emailUtils)
|
||||
.sendAlertEmail(
|
||||
"""
|
||||
Copied detail reports.
|
||||
The following errors were encountered:
|
||||
Registrar: TheRegistrar
|
||||
Error: java.io.IOException: expected
|
||||
""");
|
||||
Copied detail reports.
|
||||
The following errors were encountered:
|
||||
Registrar: TheRegistrar
|
||||
Error: java.io.IOException: expected
|
||||
""");
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
assertThat(response.getContentType()).isEqualTo(MediaType.PLAIN_TEXT_UTF_8);
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo(
|
||||
"""
|
||||
Copied detail reports.
|
||||
The following errors were encountered:
|
||||
Registrar: TheRegistrar
|
||||
Error: java.io.IOException: expected
|
||||
""");
|
||||
Copied detail reports.
|
||||
The following errors were encountered:
|
||||
Registrar: TheRegistrar
|
||||
Error: java.io.IOException: expected
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFail_tooManyFailures_one_registrar_sendsAlertEmail_continues() throws IOException {
|
||||
gcsUtils.createFromBytes(
|
||||
BlobId.of("test-bucket", "results/invoice_details_2017-10_TheRegistrar_hello.csv"),
|
||||
"hola,mundo\n3,4".getBytes(UTF_8));
|
||||
|
||||
gcsUtils.createFromBytes(
|
||||
BlobId.of("test-bucket", "results/invoice_details_2017-10_TheRegistrar_test.csv"),
|
||||
"hello,world\n1,2".getBytes(UTF_8));
|
||||
when(driveConnection.createOrUpdateFile(any(), any(), any(), any()))
|
||||
.thenThrow(new IOException("expected"));
|
||||
|
||||
action.run();
|
||||
verify(emailUtils)
|
||||
.sendAlertEmail(
|
||||
"""
|
||||
Copied detail reports.
|
||||
The following errors were encountered:
|
||||
Registrar: TheRegistrar
|
||||
Error: java.io.IOException: expected
|
||||
\tjava.io.IOException: expected
|
||||
""");
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
assertThat(response.getContentType()).isEqualTo(MediaType.PLAIN_TEXT_UTF_8);
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo(
|
||||
"""
|
||||
Copied detail reports.
|
||||
The following errors were encountered:
|
||||
Registrar: TheRegistrar
|
||||
Error: java.io.IOException: expected
|
||||
\tjava.io.IOException: expected
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -20,6 +20,7 @@ import static google.registry.testing.DatabaseHelper.loadRegistrar;
|
||||
import static google.registry.testing.DatabaseHelper.loadSingleton;
|
||||
import static google.registry.testing.SqlHelper.saveRegistrar;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.when;
|
||||
@@ -54,30 +55,52 @@ class SecurityActionTest extends ConsoleActionBaseTestCase {
|
||||
SAMPLE_CERT2);
|
||||
private Registrar testRegistrar;
|
||||
|
||||
private static final String VALIDITY_TOO_LONG_CERT_PEM =
|
||||
"-----BEGIN CERTIFICATE-----\n"
|
||||
+ "MIIDejCCAv+gAwIBAgIQHNcSEt4VENkSgtozEEoQLzAKBggqhkjOPQQDAzB8MQsw\n"
|
||||
+ "CQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24xGDAW\n"
|
||||
+ "BgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBSb290IENl\n"
|
||||
+ "cnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzAeFw0xOTAzMDcxOTQyNDJaFw0zNDAz\n"
|
||||
+ "MDMxOTQyNDJaMG8xCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UE\n"
|
||||
+ "BwwHSG91c3RvbjERMA8GA1UECgwIU1NMIENvcnAxKzApBgNVBAMMIlNTTC5jb20g\n"
|
||||
+ "U1NMIEludGVybWVkaWF0ZSBDQSBFQ0MgUjIwdjAQBgcqhkjOPQIBBgUrgQQAIgNi\n"
|
||||
+ "AASEOWn30uEYKDLFu4sCjFQ1VupFaeMtQjqVWyWSA7+KFljnsVaFQ2hgs4cQk1f/\n"
|
||||
+ "RQ2INSwdVCYU0i5qsbom20rigUhDh9dM/r6bEZ75eFE899kSCI14xqThYVLPdLEl\n"
|
||||
+ "+dyjggFRMIIBTTASBgNVHRMBAf8ECDAGAQH/AgEAMB8GA1UdIwQYMBaAFILRhXMw\n"
|
||||
+ "5zUE044CkvvlpNHEIejNMHgGCCsGAQUFBwEBBGwwajBGBggrBgEFBQcwAoY6aHR0\n"
|
||||
+ "cDovL3d3dy5zc2wuY29tL3JlcG9zaXRvcnkvU1NMY29tLVJvb3RDQS1FQ0MtMzg0\n"
|
||||
+ "LVIxLmNydDAgBggrBgEFBQcwAYYUaHR0cDovL29jc3BzLnNzbC5jb20wEQYDVR0g\n"
|
||||
+ "BAowCDAGBgRVHSAAMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATA7BgNV\n"
|
||||
+ "HR8ENDAyMDCgLqAshipodHRwOi8vY3Jscy5zc2wuY29tL3NzbC5jb20tZWNjLVJv\n"
|
||||
+ "b3RDQS5jcmwwHQYDVR0OBBYEFA10Zgpen+Is7NXCXSUEf3Uyuv99MA4GA1UdDwEB\n"
|
||||
+ "/wQEAwIBhjAKBggqhkjOPQQDAwNpADBmAjEAxYt6Ylk/N8Fch/3fgKYKwI5A011Q\n"
|
||||
+ "MKW0h3F9JW/NX/F7oYtWrxljheH8n2BrkDybAjEAlCxkLE0vQTYcFzrR24oogyw6\n"
|
||||
+ "VkgTm92+jiqJTO5SSA9QUa092S5cTKiHkH2cOM6m\n"
|
||||
+ "-----END CERTIFICATE-----";
|
||||
|
||||
private AuthenticatedRegistrarAccessor registrarAccessor =
|
||||
AuthenticatedRegistrarAccessor.createForTesting(
|
||||
ImmutableSetMultimap.of("registrarId", AuthenticatedRegistrarAccessor.Role.ADMIN));
|
||||
|
||||
private CertificateChecker certificateChecker =
|
||||
new CertificateChecker(
|
||||
ImmutableSortedMap.of(START_OF_TIME, 20825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
|
||||
30,
|
||||
15,
|
||||
2048,
|
||||
ImmutableSet.of("secp256r1", "secp384r1"),
|
||||
clock);
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
testRegistrar = saveRegistrar("registrarId");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_postRegistrarInfo() throws IOException {
|
||||
CertificateChecker lenientChecker =
|
||||
new CertificateChecker(
|
||||
ImmutableSortedMap.of(
|
||||
START_OF_TIME, 20825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
|
||||
30,
|
||||
15,
|
||||
2048,
|
||||
ImmutableSet.of("secp256r1", "secp384r1"),
|
||||
clock);
|
||||
|
||||
clock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
|
||||
SecurityAction action =
|
||||
createAction(
|
||||
testRegistrar.getRegistrarId());
|
||||
SecurityAction action = createAction(testRegistrar.getRegistrarId(), lenientChecker);
|
||||
action.run();
|
||||
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK);
|
||||
Registrar r = loadRegistrar(testRegistrar.getRegistrarId());
|
||||
@@ -90,9 +113,39 @@ class SecurityActionTest extends ConsoleActionBaseTestCase {
|
||||
assertThat(history.getDescription()).hasValue("registrarId|IP_CHANGE,PRIMARY_SSL_CERT_CHANGE");
|
||||
}
|
||||
|
||||
private SecurityAction createAction(String registrarId) throws IOException {
|
||||
@Test
|
||||
void testFailure_validityPeriodTooLong_returnsSpecificError() throws IOException {
|
||||
CertificateChecker strictChecker =
|
||||
new CertificateChecker(
|
||||
ImmutableSortedMap.of(START_OF_TIME, 398),
|
||||
30,
|
||||
15,
|
||||
2048,
|
||||
ImmutableSet.of("secp256r1", "secp384r1"),
|
||||
clock);
|
||||
|
||||
clock.setTo(DateTime.parse("2025-01-01T00:00:00Z"));
|
||||
String escapedCert = VALIDITY_TOO_LONG_CERT_PEM.replace("\n", "\\n");
|
||||
String jsonWithBadCert =
|
||||
String.format(
|
||||
"{\"registrarId\": \"registrarId\", \"clientCertificate\": \"%s\"}", escapedCert);
|
||||
|
||||
SecurityAction action =
|
||||
createAction(testRegistrar.getRegistrarId(), jsonWithBadCert, strictChecker);
|
||||
action.run();
|
||||
|
||||
String expectedError =
|
||||
"Certificate validity period is too long; it must be less than or equal to 398 days.";
|
||||
FakeResponse response = (FakeResponse) consoleApiParams.response();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_BAD_REQUEST);
|
||||
assertThat(response.getPayload()).isEqualTo(expectedError);
|
||||
}
|
||||
|
||||
private SecurityAction createAction(
|
||||
String registrarId, String jsonBody, CertificateChecker certificateChecker)
|
||||
throws IOException {
|
||||
when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.POST.toString());
|
||||
doReturn(new BufferedReader(new StringReader(jsonRegistrar1)))
|
||||
doReturn(new BufferedReader(new StringReader(jsonBody)))
|
||||
.when(consoleApiParams.request())
|
||||
.getReader();
|
||||
Optional<Registrar> maybeRegistrar =
|
||||
@@ -101,4 +154,9 @@ class SecurityActionTest extends ConsoleActionBaseTestCase {
|
||||
return new SecurityAction(
|
||||
consoleApiParams, certificateChecker, registrarAccessor, registrarId, maybeRegistrar);
|
||||
}
|
||||
|
||||
private SecurityAction createAction(String registrarId, CertificateChecker certificateChecker)
|
||||
throws IOException {
|
||||
return createAction(registrarId, jsonRegistrar1, certificateChecker);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ spec:
|
||||
value: /_dr/epptool
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /loadtest
|
||||
value: /_dr/loadtest
|
||||
backendRefs:
|
||||
- group: net.gke.io
|
||||
kind: ServiceImport
|
||||
@@ -58,7 +58,7 @@ spec:
|
||||
value: "true"
|
||||
- path:
|
||||
type: PathPrefix
|
||||
value: /loadtest
|
||||
value: /_dr/loadtest
|
||||
headers:
|
||||
- name: "canary"
|
||||
value: "true"
|
||||
|
||||
@@ -14,9 +14,6 @@ spec:
|
||||
service: backend
|
||||
spec:
|
||||
serviceAccountName: nomulus
|
||||
nodeSelector:
|
||||
cloud.google.com/compute-class: "Performance"
|
||||
cloud.google.com/machine-family: c4
|
||||
containers:
|
||||
- name: backend
|
||||
image: gcr.io/GCP_PROJECT/nomulus
|
||||
@@ -25,8 +22,17 @@ spec:
|
||||
name: http
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "512Mi"
|
||||
# explicit pod-slots 0 is required in order to downgrade node
|
||||
# class from performance, which has implicit pod-slots 1
|
||||
cloud.google.com/pod-slots: 0
|
||||
cpu: "500m"
|
||||
memory: "1Gi"
|
||||
limits:
|
||||
# explicit pod-slots 0 is required in order to downgrade node
|
||||
# class from performance, which has implicit pod-slots 1
|
||||
cloud.google.com/pod-slots: 0
|
||||
cpu: "1000m"
|
||||
memory: "1Gi"
|
||||
args: [ENVIRONMENT]
|
||||
env:
|
||||
- name: POD_ID
|
||||
@@ -61,7 +67,7 @@ spec:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 100
|
||||
averageUtilization: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
|
||||
@@ -41,7 +41,7 @@ spec:
|
||||
# class from performance, which has implicit pod-slots 1
|
||||
cloud.google.com/pod-slots: 0
|
||||
cpu: "500m"
|
||||
memory: "2Gi"
|
||||
memory: "1Gi"
|
||||
args: [ENVIRONMENT]
|
||||
env:
|
||||
- name: POD_ID
|
||||
@@ -76,7 +76,7 @@ spec:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 100
|
||||
averageUtilization: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
|
||||
@@ -14,9 +14,6 @@ spec:
|
||||
service: frontend
|
||||
spec:
|
||||
serviceAccountName: nomulus
|
||||
nodeSelector:
|
||||
cloud.google.com/compute-class: "Performance"
|
||||
cloud.google.com/machine-family: c4
|
||||
containers:
|
||||
- name: frontend
|
||||
image: gcr.io/GCP_PROJECT/nomulus
|
||||
@@ -25,8 +22,17 @@ spec:
|
||||
name: http
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
# explicit pod-slots 0 is required in order to downgrade node
|
||||
# class from performance, which has implicit pod-slots 1
|
||||
cloud.google.com/pod-slots: 0
|
||||
cpu: "1000m"
|
||||
memory: "1Gi"
|
||||
limits:
|
||||
# explicit pod-slots 0 is required in order to downgrade node
|
||||
# class from performance, which has implicit pod-slots 1
|
||||
cloud.google.com/pod-slots: 0
|
||||
cpu: "1000m"
|
||||
memory: "2Gi"
|
||||
args: [ENVIRONMENT]
|
||||
env:
|
||||
- name: POD_ID
|
||||
@@ -50,7 +56,16 @@ spec:
|
||||
name: epp
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
# explicit pod-slots 0 is required in order to downgrade node
|
||||
# class from performance, which has implicit pod-slots 1
|
||||
cloud.google.com/pod-slots: 0
|
||||
cpu: "1000m"
|
||||
memory: "512Mi"
|
||||
limits:
|
||||
# explicit pod-slots 0 is required in order to downgrade node
|
||||
# class from performance, which has implicit pod-slots 1
|
||||
cloud.google.com/pod-slots: 0
|
||||
cpu: "1000m"
|
||||
memory: "512Mi"
|
||||
args: [--env, PROXY_ENV, --log, --local]
|
||||
env:
|
||||
@@ -85,12 +100,12 @@ spec:
|
||||
minReplicas: 5
|
||||
maxReplicas: 20
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 100
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
|
||||
@@ -34,13 +34,13 @@ spec:
|
||||
# explicit pod-slots 0 is required in order to downgrade node
|
||||
# class from performance, which has implicit pod-slots 1
|
||||
cloud.google.com/pod-slots: 0
|
||||
cpu: "100m"
|
||||
cpu: "500m"
|
||||
memory: "1Gi"
|
||||
limits:
|
||||
# explicit pod-slots 0 is required in order to downgrade node
|
||||
# class from performance, which has implicit pod-slots 1
|
||||
cloud.google.com/pod-slots: 0
|
||||
cpu: "500m"
|
||||
cpu: "1000m"
|
||||
memory: "2Gi"
|
||||
args: [ENVIRONMENT]
|
||||
env:
|
||||
@@ -76,7 +76,7 @@ spec:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 100
|
||||
averageUtilization: 80
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
|
||||
@@ -33,6 +33,7 @@ do
|
||||
--project "${project}" --zone "${parts[1]}"
|
||||
sed s/GCP_PROJECT/${project}/g "./kubernetes/proxy-deployment-${environment}.yaml" | \
|
||||
kubectl apply -f -
|
||||
kubectl apply -f "./kubernetes/proxy-limit-range.yaml" --force
|
||||
kubectl apply -f "./kubernetes/proxy-service.yaml" --force
|
||||
# Alpha does not have canary
|
||||
if [[ ${environment} != "alpha" ]]; then
|
||||
|
||||
14
proxy/kubernetes/proxy-limit-range.yaml
Normal file
14
proxy/kubernetes/proxy-limit-range.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: LimitRange
|
||||
metadata:
|
||||
name: resource-limits
|
||||
namespace: default
|
||||
spec:
|
||||
limits:
|
||||
- type: Container
|
||||
default:
|
||||
cpu: "300m"
|
||||
memory: "512Mi"
|
||||
defaultRequest:
|
||||
cpu: "100m"
|
||||
memory: "350Mi"
|
||||
@@ -33,3 +33,10 @@ spec:
|
||||
name: proxy-deployment-canary
|
||||
maxReplicas: 10
|
||||
minReplicas: 1
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 100
|
||||
|
||||
@@ -33,3 +33,11 @@ spec:
|
||||
name: proxy-deployment
|
||||
maxReplicas: 50
|
||||
minReplicas: 10
|
||||
metrics:
|
||||
- type: Resource
|
||||
resource:
|
||||
name: cpu
|
||||
target:
|
||||
type: Utilization
|
||||
averageUtilization: 100
|
||||
|
||||
|
||||
@@ -19,8 +19,11 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static google.registry.networking.handler.SslServerInitializer.CLIENT_CERTIFICATE_PROMISE_KEY;
|
||||
import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
|
||||
import static google.registry.util.X509Utils.getCertificateHash;
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import google.registry.proxy.metric.FrontendMetrics;
|
||||
import google.registry.util.ProxyHttpHeaders;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
@@ -36,7 +39,11 @@ import io.netty.handler.ssl.SslHandshakeCompletionEvent;
|
||||
import io.netty.util.AttributeKey;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Handler that processes EPP protocol logic. */
|
||||
public class EppServiceHandler extends HttpsRelayServiceHandler {
|
||||
@@ -57,6 +64,8 @@ public class EppServiceHandler extends HttpsRelayServiceHandler {
|
||||
private String sslClientCertificateHash;
|
||||
private String clientAddress;
|
||||
|
||||
private Optional<String> maybeRegistrarId = Optional.empty();
|
||||
|
||||
public EppServiceHandler(
|
||||
String relayHost,
|
||||
String relayPath,
|
||||
@@ -128,6 +137,9 @@ public class EppServiceHandler extends HttpsRelayServiceHandler {
|
||||
.set(ProxyHttpHeaders.FALLBACK_IP_ADDRESS, clientAddress)
|
||||
.set(HttpHeaderNames.CONTENT_TYPE, EPP_CONTENT_TYPE)
|
||||
.set(HttpHeaderNames.ACCEPT, EPP_CONTENT_TYPE);
|
||||
|
||||
maybeSetRegistrarIdHeader(request);
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
@@ -142,4 +154,54 @@ public class EppServiceHandler extends HttpsRelayServiceHandler {
|
||||
}
|
||||
super.write(ctx, msg, promise);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets and caches the Registrar-ID header on the request if the ID can be found.
|
||||
*
|
||||
* <p>This method first checks if the registrar ID has already been determined. If not, it
|
||||
* inspects the cookies for a "SESSION_INFO" cookie, from which it attempts to extract the
|
||||
* registrar ID.
|
||||
*
|
||||
* @param request The {@link FullHttpRequest} on which to potentially set the registrar ID header.
|
||||
* @see #extractRegistrarIdFromSessionInfo(String)
|
||||
*/
|
||||
private void maybeSetRegistrarIdHeader(FullHttpRequest request) {
|
||||
if (maybeRegistrarId.isEmpty()) {
|
||||
maybeRegistrarId =
|
||||
cookieStore.entrySet().stream()
|
||||
.map(e -> e.getValue())
|
||||
.filter(cookie -> "SESSION_INFO".equals(cookie.name()))
|
||||
.findFirst()
|
||||
.flatMap(cookie -> extractRegistrarIdFromSessionInfo(cookie.value()));
|
||||
}
|
||||
|
||||
if (maybeRegistrarId.isPresent() && !Strings.isNullOrEmpty(maybeRegistrarId.get())) {
|
||||
request.headers().set(ProxyHttpHeaders.REGISTRAR_ID, maybeRegistrarId.get());
|
||||
}
|
||||
}
|
||||
|
||||
/** Extracts the registrar ID from a Base64-encoded session info string. */
|
||||
private Optional<String> extractRegistrarIdFromSessionInfo(@Nullable String sessionInfo) {
|
||||
if (sessionInfo == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
try {
|
||||
String decodedString = new String(BaseEncoding.base64Url().decode(sessionInfo), US_ASCII);
|
||||
Pattern pattern = Pattern.compile("clientId=([^,\\s]+)?");
|
||||
Matcher matcher = pattern.matcher(decodedString);
|
||||
|
||||
if (matcher.find()) {
|
||||
String maybeRegistrarIdMatch = matcher.group(1);
|
||||
if (!maybeRegistrarIdMatch.equals("null")) {
|
||||
return Optional.of(maybeRegistrarIdMatch);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Throwable e) {
|
||||
logger.atSevere().withCause(e).log("Failed to decode session info from Base64");
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec<FullHt
|
||||
protected static final ImmutableSet<Class<? extends Exception>> NON_FATAL_OUTBOUND_EXCEPTIONS =
|
||||
ImmutableSet.of(NonOkHttpResponseException.class);
|
||||
|
||||
private final Map<String, Cookie> cookieStore = new LinkedHashMap<>();
|
||||
protected final Map<String, Cookie> cookieStore = new LinkedHashMap<>();
|
||||
private final String relayHost;
|
||||
private final String relayPath;
|
||||
private final boolean canary;
|
||||
|
||||
@@ -20,6 +20,7 @@ import static google.registry.proxy.TestUtils.assertHttpRequestEquivalent;
|
||||
import static google.registry.proxy.TestUtils.makeEppHttpResponse;
|
||||
import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
|
||||
import static google.registry.util.X509Utils.getCertificateHash;
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -27,6 +28,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import google.registry.proxy.TestUtils;
|
||||
import google.registry.proxy.handler.HttpsRelayServiceHandler.NonOkHttpResponseException;
|
||||
import google.registry.proxy.metric.FrontendMetrics;
|
||||
@@ -357,4 +359,82 @@ class EppServiceHandlerTest {
|
||||
assertThat((Object) channel.readOutbound()).isNull();
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_registrarIdHeader_isSetFromSessionInfoCookie() throws Exception {
|
||||
setHandshakeSuccess();
|
||||
channel.readInbound(); // Read and discard the initial hello request.
|
||||
|
||||
// Simulate a server response that sets the SESSION_INFO cookie.
|
||||
String registrarId = "TheRegistrar";
|
||||
String sessionInfoValue =
|
||||
BaseEncoding.base64Url()
|
||||
.encode(("alpha,clientId=" + registrarId + ",beta").getBytes(US_ASCII));
|
||||
Cookie sessionCookie = new DefaultCookie("SESSION_INFO", sessionInfoValue);
|
||||
channel.writeOutbound(
|
||||
makeEppHttpResponse("<epp>greeting</epp>", HttpResponseStatus.OK, sessionCookie));
|
||||
channel.readOutbound(); // Read and discard the response sent to the client.
|
||||
|
||||
// Simulate a subsequent client request and check for the registrar ID header.
|
||||
String clientRequestContent = "<epp>login</epp>";
|
||||
channel.writeInbound(Unpooled.wrappedBuffer(clientRequestContent.getBytes(UTF_8)));
|
||||
|
||||
FullHttpRequest relayedRequest = channel.readInbound();
|
||||
FullHttpRequest expectedRequest = makeEppHttpRequest(clientRequestContent, sessionCookie);
|
||||
expectedRequest.headers().set(ProxyHttpHeaders.REGISTRAR_ID, registrarId);
|
||||
|
||||
assertHttpRequestEquivalent(relayedRequest, expectedRequest);
|
||||
assertThat((Object) channel.readInbound()).isNull();
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_registrarIdHeader_isNotSetWhenSessionInfoCookieIsMissing() throws Exception {
|
||||
setHandshakeSuccess();
|
||||
channel.readInbound(); // Read and discard the initial hello request.
|
||||
|
||||
// Simulate a server response that does NOT set the SESSION_INFO cookie.
|
||||
Cookie otherCookie = new DefaultCookie("some_other_cookie", "some_value");
|
||||
channel.writeOutbound(
|
||||
makeEppHttpResponse("<epp>greeting</epp>", HttpResponseStatus.OK, otherCookie));
|
||||
channel.readOutbound(); // Read and discard the response sent to the client.
|
||||
|
||||
// Simulate a subsequent client request and verify the header is absent.
|
||||
String clientRequestContent = "<epp>login</epp>";
|
||||
channel.writeInbound(Unpooled.wrappedBuffer(clientRequestContent.getBytes(UTF_8)));
|
||||
|
||||
FullHttpRequest relayedRequest = channel.readInbound();
|
||||
FullHttpRequest expectedRequest = makeEppHttpRequest(clientRequestContent, otherCookie);
|
||||
|
||||
assertHttpRequestEquivalent(relayedRequest, expectedRequest);
|
||||
assertThat(relayedRequest.headers().contains(ProxyHttpHeaders.REGISTRAR_ID)).isFalse();
|
||||
assertThat((Object) channel.readInbound()).isNull();
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_registrarIdHeader_isNotSetWhenClientIdIsNull() throws Exception {
|
||||
setHandshakeSuccess();
|
||||
channel.readInbound(); // Read and discard the initial hello request.
|
||||
|
||||
// Simulate a server response with a SESSION_INFO cookie where clientId is "null".
|
||||
String sessionInfoValue =
|
||||
BaseEncoding.base64Url().encode("alpha,clientId=null,beta".getBytes(US_ASCII));
|
||||
Cookie sessionCookie = new DefaultCookie("SESSION_INFO", sessionInfoValue);
|
||||
channel.writeOutbound(
|
||||
makeEppHttpResponse("<epp>greeting</epp>", HttpResponseStatus.OK, sessionCookie));
|
||||
channel.readOutbound(); // Read and discard the response sent to the client.
|
||||
|
||||
// Simulate a subsequent client request and verify the header is absent.
|
||||
String clientRequestContent = "<epp>login</epp>";
|
||||
channel.writeInbound(Unpooled.wrappedBuffer(clientRequestContent.getBytes(UTF_8)));
|
||||
|
||||
FullHttpRequest relayedRequest = channel.readInbound();
|
||||
FullHttpRequest expectedRequest = makeEppHttpRequest(clientRequestContent, sessionCookie);
|
||||
|
||||
assertHttpRequestEquivalent(relayedRequest, expectedRequest);
|
||||
assertThat(relayedRequest.headers().contains(ProxyHttpHeaders.REGISTRAR_ID)).isFalse();
|
||||
assertThat((Object) channel.readInbound()).isNull();
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ public final class ProxyHttpHeaders {
|
||||
/** HTTP header name used to pass the client IP address from the proxy to Nomulus. */
|
||||
public static final String IP_ADDRESS = "Nomulus-Client-Address";
|
||||
|
||||
/** HTTP header name used to pass the Registrar Id from the proxy to Nomulus. */
|
||||
public static final String REGISTRAR_ID = "Nomulus-Registrar-Id";
|
||||
|
||||
/**
|
||||
* Fallback HTTP header name used to pass the client IP address from the proxy to Nomulus.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user