1
0
mirror of https://github.com/google/nomulus synced 2026-05-25 01:01:57 +00:00

Compare commits

...

7 Commits

Author SHA1 Message Date
Pavlo Tkach
5cef2dd8b5 Remove nodeSelector from k8s deployments (#2798)
nodeSelector can limit scheduling capabilities of k8s, which leads to delays in assigning new workloads. Since we do not require and particular machine for execution it can be removed.
2025-08-10 16:16:48 +00:00
gbrodman
62b2585220 Fix load-testing URL in backend routing k8s file (#2797) 2025-08-08 20:20:40 +00:00
sharma1210
8692fe35db Provide specific reason for invalid SSL certificate (#2792)
* Fix: Robustly parse certs and provide specific errors

* Add test for expired certificate failure

* fixing indentation

* fixing indentation

* Update SecurityActionTest.java

* Update SecurityActionTest.java for correcting the testcase

* Fix: Provide indentation fix

* Fixing Deduplication in test
2025-08-08 18:41:14 +00:00
Pavlo Tkach
18614ba11e Update resource allocation for all Nomulus GKE deployments (#2796)
ALl deployments received update to averageUtilization cpu. This should allow us to stay ahead of the curve of traffic and create instances before we cpu reached the limit.
Frontend cpu allocation has caused "noise neighbors" problem with pods assigned to nodes where there's not enough bursting capacity, so I increased it.
Adjusted rest of the deployments according to their utilization.
2025-08-08 17:55:08 +00:00
Pavlo Tkach
427f6db820 Update resource allocation for proxy deployment (#2794)
Missing resource requests as well as metrics for when to evict resource
produced situation when under load k8s struggled to assign pods. This
adds default resource requirements based on 2 weeks metrics and
instructions when resource should be evicted.
2025-08-08 15:35:22 +00:00
Weimin Yu
5aa40b2208 Fix error handling in CopyDetailReportsAction (#2793)
* Fix error handling in CopyDetailReportsAction

The action tries to record errors per registrar in an ImmutableMap, without realizing that
there may be duplicate keys due to retries.

Switched to the `buildKeepingLast` method to build the map.

* Addressing comments and rebase
2025-08-06 16:43:29 +00:00
Pavlo Tkach
95c89bc856 Add registrar id header to proxy requests (#2791) 2025-08-05 17:57:04 +00:00
17 changed files with 353 additions and 62 deletions

View File

@@ -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());

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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"

View File

@@ -33,3 +33,10 @@ spec:
name: proxy-deployment-canary
maxReplicas: 10
minReplicas: 1
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 100

View File

@@ -33,3 +33,11 @@ spec:
name: proxy-deployment
maxReplicas: 50
minReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 100

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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();
}
}

View File

@@ -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.
*