mirror of
https://github.com/google/nomulus
synced 2026-08-07 15:56:08 +00:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76bd13ddf0 | ||
|
|
8cbf3242a7 | ||
|
|
b1f2eb5921 | ||
|
|
4f332d397e | ||
|
|
c9a82f1322 | ||
|
|
fabf0c07b2 | ||
|
|
aa54f9ddc9 | ||
|
|
92edbfbde2 | ||
|
|
b1e127798f | ||
|
|
3474cd6e9b | ||
|
|
1fc4a281c0 | ||
|
|
9a420a69b0 | ||
|
|
a3421f2999 |
@@ -28,6 +28,9 @@ import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.net.MediaType;
|
||||
import com.google.monitoring.metrics.IncrementableMetric;
|
||||
import com.google.monitoring.metrics.LabelDescriptor;
|
||||
import com.google.monitoring.metrics.MetricRegistryImpl;
|
||||
import google.registry.cache.SimplifiedJedisClient;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.common.Cursor;
|
||||
@@ -39,6 +42,7 @@ import google.registry.request.Action;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.request.lock.LockHandler;
|
||||
import google.registry.util.NonFinalForTesting;
|
||||
import jakarta.inject.Inject;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
@@ -61,6 +65,25 @@ public class SyncRemoteCacheAction implements Runnable {
|
||||
private static final String LOCK_NAME = "syncRemoteCacheAction";
|
||||
private static final int BATCH_SIZE = 10000;
|
||||
|
||||
public enum SyncStatus {
|
||||
SUCCESS,
|
||||
FAILURE,
|
||||
NOT_CONFIGURED
|
||||
}
|
||||
|
||||
private static final ImmutableSet<LabelDescriptor> LABEL_DESCRIPTORS =
|
||||
ImmutableSet.of(
|
||||
LabelDescriptor.create("status", "Whether SyncRemoteCacheAction succeeded or failed."));
|
||||
|
||||
@NonFinalForTesting
|
||||
static final IncrementableMetric SYNC_CACHE_RUNS_METRIC =
|
||||
MetricRegistryImpl.getDefault()
|
||||
.newIncrementableMetric(
|
||||
"/batch/sync_remote_cache/runs",
|
||||
"Count of SyncRemoteCacheAction executions",
|
||||
"count",
|
||||
LABEL_DESCRIPTORS);
|
||||
|
||||
private final LockHandler lockHandler;
|
||||
private final Response response;
|
||||
private final Optional<SimplifiedJedisClient> jedisClient;
|
||||
@@ -79,14 +102,17 @@ public class SyncRemoteCacheAction implements Runnable {
|
||||
if (jedisClient.isEmpty()) {
|
||||
response.setStatus(SC_NO_CONTENT);
|
||||
response.setPayload("No Jedis/Valkey configuration found");
|
||||
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.NOT_CONFIGURED.name());
|
||||
return;
|
||||
}
|
||||
Callable<Void> runner =
|
||||
() -> {
|
||||
try {
|
||||
runLocked();
|
||||
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.SUCCESS.name());
|
||||
response.setStatus(SC_OK);
|
||||
} catch (Exception e) {
|
||||
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.FAILURE.name());
|
||||
logger.atSevere().withCause(e).log("Errored out during execution.");
|
||||
response.setStatus(SC_INTERNAL_SERVER_ERROR);
|
||||
response.setPayload(String.format("Errored out with cause: %s", e));
|
||||
@@ -95,6 +121,7 @@ public class SyncRemoteCacheAction implements Runnable {
|
||||
};
|
||||
|
||||
if (!lockHandler.executeWithLocks(runner, null, Duration.ofHours(1), LOCK_NAME)) {
|
||||
SYNC_CACHE_RUNS_METRIC.increment(SyncStatus.FAILURE.name());
|
||||
// Send a 200-series status code to prevent this conflicting action from retrying.
|
||||
response.setStatus(SC_NO_CONTENT);
|
||||
response.setPayload("Could not acquire lock; already running?");
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
package google.registry.beam.spec11;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import dagger.Component;
|
||||
@@ -25,11 +24,9 @@ import google.registry.beam.common.RegistryJpaIO;
|
||||
import google.registry.beam.common.RegistryJpaIO.Read;
|
||||
import google.registry.beam.spec11.SafeBrowsingTransforms.EvaluateSafeBrowsingFn;
|
||||
import google.registry.config.RegistryConfig.ConfigModule;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.reporting.Spec11ThreatMatch;
|
||||
import google.registry.model.reporting.Spec11ThreatMatch.ThreatType;
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.UtilsModule;
|
||||
@@ -39,8 +36,7 @@ import java.time.LocalDate;
|
||||
import java.time.YearMonth;
|
||||
import org.apache.beam.sdk.Pipeline;
|
||||
import org.apache.beam.sdk.PipelineResult;
|
||||
import org.apache.beam.sdk.coders.KvCoder;
|
||||
import org.apache.beam.sdk.coders.StringUtf8Coder;
|
||||
import org.apache.beam.sdk.coders.SerializableCoder;
|
||||
import org.apache.beam.sdk.io.TextIO;
|
||||
import org.apache.beam.sdk.options.PipelineOptionsFactory;
|
||||
import org.apache.beam.sdk.transforms.DoFn;
|
||||
@@ -112,44 +108,22 @@ public class Spec11Pipeline implements Serializable {
|
||||
}
|
||||
|
||||
static PCollection<DomainNameInfo> readFromCloudSql(Pipeline pipeline) {
|
||||
Read<Object[], KV<String, String>> read =
|
||||
Read<Object[], DomainNameInfo> read =
|
||||
RegistryJpaIO.read(
|
||||
"select d.repoId, r.emailAddress from Domain d join Registrar r on"
|
||||
+ " d.currentSponsorRegistrarId = r.registrarId where r.type = 'REAL' and"
|
||||
+ " d.deletionTime > CAST(now() AS timestamp)",
|
||||
"""
|
||||
SELECT d.domainName, d.repoId, d.currentSponsorRegistrarId, r.emailAddress FROM
|
||||
Domain d JOIN Registrar r ON d.currentSponsorRegistrarId = r.registrarId WHERE
|
||||
r.type = 'REAL' AND d.deletionTime > CAST(now() AS timestamp)
|
||||
""",
|
||||
false,
|
||||
Spec11Pipeline::parseRow)
|
||||
.withCoder(KvCoder.of(StringUtf8Coder.of(), StringUtf8Coder.of()));
|
||||
|
||||
return pipeline
|
||||
.apply("Read active domains from Cloud SQL", read)
|
||||
.apply(
|
||||
"Build DomainNameInfo",
|
||||
ParDo.of(
|
||||
new DoFn<KV<String, String>, DomainNameInfo>() {
|
||||
@ProcessElement
|
||||
public void processElement(
|
||||
@Element KV<String, String> input, OutputReceiver<DomainNameInfo> output) {
|
||||
Domain domain =
|
||||
tm().transact(
|
||||
() -> tm().loadByKey(VKey.create(Domain.class, input.getKey())));
|
||||
String emailAddress = input.getValue();
|
||||
if (emailAddress == null) {
|
||||
emailAddress = "";
|
||||
}
|
||||
DomainNameInfo domainNameInfo =
|
||||
DomainNameInfo.create(
|
||||
domain.getDomainName(),
|
||||
domain.getRepoId(),
|
||||
domain.getCurrentSponsorRegistrarId(),
|
||||
emailAddress);
|
||||
output.output(domainNameInfo);
|
||||
}
|
||||
}));
|
||||
.withCoder(SerializableCoder.of(DomainNameInfo.class));
|
||||
return pipeline.apply("Read active domains from Cloud SQL", read);
|
||||
}
|
||||
|
||||
private static KV<String, String> parseRow(Object[] row) {
|
||||
return KV.of((String) row[0], (String) row[1]);
|
||||
private static DomainNameInfo parseRow(Object[] row) {
|
||||
String emailAddress = row[3] != null ? (String) row[3] : "";
|
||||
return new DomainNameInfo((String) row[0], (String) row[1], (String) row[2], emailAddress);
|
||||
}
|
||||
|
||||
static void saveToSql(
|
||||
|
||||
@@ -14,14 +14,18 @@
|
||||
|
||||
package google.registry.dns;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
|
||||
import static google.registry.dns.RefreshDnsOnHostRenameAction.PATH;
|
||||
import static google.registry.model.EppResourceUtils.getLinkedDomainKeys;
|
||||
import static google.registry.model.EppResourceUtils.isDeleted;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.model.EppResourceUtils;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.host.Host;
|
||||
import google.registry.persistence.VKey;
|
||||
@@ -29,8 +33,11 @@ import google.registry.request.Action;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Clock;
|
||||
import jakarta.inject.Inject;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Action(
|
||||
service = Action.Service.BACKEND,
|
||||
@@ -43,45 +50,52 @@ public class RefreshDnsOnHostRenameAction implements Runnable {
|
||||
public static final String PARAM_HOST_KEY = "hostKey";
|
||||
public static final String PATH = "/_dr/task/refreshDnsOnHostRename";
|
||||
|
||||
private static final int DNS_REFRESH_BATCH_SIZE = 1000;
|
||||
|
||||
private final VKey<Host> hostKey;
|
||||
private final Response response;
|
||||
private final Clock clock;
|
||||
|
||||
@Inject
|
||||
RefreshDnsOnHostRenameAction(@Parameter(PARAM_HOST_KEY) String hostKey, Response response) {
|
||||
RefreshDnsOnHostRenameAction(
|
||||
@Parameter(PARAM_HOST_KEY) String hostKey, Response response, Clock clock) {
|
||||
this.hostKey = VKey.createEppVKeyFromString(hostKey);
|
||||
this.response = response;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
tm().transact(
|
||||
() -> {
|
||||
Instant now = tm().getTxTime();
|
||||
Host host = tm().loadByKeyIfPresent(hostKey).orElse(null);
|
||||
boolean hostValid = true;
|
||||
String failureMessage = null;
|
||||
if (host == null) {
|
||||
hostValid = false;
|
||||
failureMessage = String.format("Host to refresh does not exist: %s", hostKey);
|
||||
} else if (EppResourceUtils.isDeleted(host, now)) {
|
||||
hostValid = false;
|
||||
failureMessage =
|
||||
String.format("Host to refresh is already deleted: %s", host.getHostName());
|
||||
} else {
|
||||
getLinkedDomainKeys(
|
||||
host.createVKey(), host.getUpdateTimestamp().getTimestamp(), null)
|
||||
.stream()
|
||||
.map(domainKey -> tm().loadByKey(domainKey))
|
||||
.filter(Domain::shouldPublishToDns)
|
||||
.forEach(domain -> requestDomainDnsRefresh(domain.getDomainName()));
|
||||
}
|
||||
Optional<Host> optionalHost = tm().transact(() -> tm().loadByKeyIfPresent(hostKey));
|
||||
if (optionalHost.isEmpty()) {
|
||||
setFailedStatus(String.format("Host to refresh does not exist: %s", hostKey));
|
||||
return;
|
||||
}
|
||||
Instant now = clock.now();
|
||||
Host host = optionalHost.get();
|
||||
if (isDeleted(host, now)) {
|
||||
setFailedStatus(String.format("Host to refresh is already deleted: %s", host.getHostName()));
|
||||
return;
|
||||
}
|
||||
ImmutableSet<VKey<Domain>> linkedDomainKeys =
|
||||
getLinkedDomainKeys(hostKey, host.getUpdateTimestamp().getTimestamp(), null);
|
||||
for (List<VKey<Domain>> batch : Iterables.partition(linkedDomainKeys, DNS_REFRESH_BATCH_SIZE)) {
|
||||
tm().transact(
|
||||
() -> {
|
||||
ImmutableSet<String> domainNames =
|
||||
tm().loadByKeysIfPresent(batch).values().stream()
|
||||
.filter(Domain::shouldPublishToDns)
|
||||
.map(Domain::getDomainName)
|
||||
.collect(toImmutableSet());
|
||||
requestDomainDnsRefresh(domainNames);
|
||||
});
|
||||
}
|
||||
response.setStatus(SC_OK);
|
||||
}
|
||||
|
||||
if (!hostValid) {
|
||||
// Set the response status code to be 204 so to not retry.
|
||||
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||
response.setStatus(SC_NO_CONTENT);
|
||||
response.setPayload(failureMessage);
|
||||
}
|
||||
});
|
||||
private void setFailedStatus(String message) {
|
||||
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
|
||||
response.setStatus(SC_NO_CONTENT);
|
||||
response.setPayload(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import dagger.multibindings.IntoSet;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryConfigSettings;
|
||||
import google.registry.eppserver.Protocol.FrontendProtocol;
|
||||
import google.registry.eppserver.handler.EppProxyProtocolHandler;
|
||||
import google.registry.eppserver.handler.EppServiceHandler;
|
||||
import google.registry.eppserver.quota.QuotaManager;
|
||||
import google.registry.networking.handler.SslServerInitializer;
|
||||
@@ -78,14 +77,12 @@ public final class EppProtocolModule {
|
||||
@Provides
|
||||
@EppProtocol
|
||||
static ImmutableList<Provider<? extends ChannelHandler>> provideHandlerProviders(
|
||||
Provider<EppProxyProtocolHandler> proxyProtocolHandlerProvider,
|
||||
@EppProtocol Provider<SslServerInitializer<NioSocketChannel>> sslServerInitializerProvider,
|
||||
@EppProtocol Provider<ReadTimeoutHandler> readTimeoutHandlerProvider,
|
||||
Provider<LengthFieldBasedFrameDecoder> lengthFieldBasedFrameDecoderProvider,
|
||||
Provider<LengthFieldPrepender> lengthFieldPrependerProvider,
|
||||
Provider<EppServiceHandler> eppServiceHandlerProvider) {
|
||||
return ImmutableList.of(
|
||||
proxyProtocolHandlerProvider,
|
||||
sslServerInitializerProvider,
|
||||
readTimeoutHandlerProvider,
|
||||
lengthFieldBasedFrameDecoderProvider,
|
||||
|
||||
@@ -1,208 +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.eppserver.handler;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.net.InetAddresses;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.handler.codec.ByteToMessageDecoder;
|
||||
import io.netty.util.AttributeKey;
|
||||
import jakarta.inject.Inject;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Handler that processes possible existence of a PROXY protocol v1 header.
|
||||
*
|
||||
* <p>When an EPP client connects to the registry (through the proxy), the registry performs two
|
||||
* validations to ensure that only known registrars are allowed. First it checks the sha265 hash of
|
||||
* the client SSL certificate and match it to the hash stored in the database for the registrar. It
|
||||
* then checks if the connection is from an allow-listed IP address that belongs to that registrar.
|
||||
*
|
||||
* <p>The proxy receives client connects via the GCP load balancer, which results in the loss of
|
||||
* original client IP from the channel. Luckily, the load balancer supports the PROXY protocol v1,
|
||||
* which adds a header with source IP information, among other things, to the TCP request at the
|
||||
* start of the connection.
|
||||
*
|
||||
* <p>This handler determines if a connection is proxied (PROXY protocol v1 header present) and
|
||||
* correctly sets the source IP address to the channel's attribute regardless of whether it is
|
||||
* proxied. After that it removes itself from the channel pipeline because the proxy header is only
|
||||
* present at the beginning of the connection.
|
||||
*
|
||||
* <p>This handler must be the very first handler in a protocol, even before SSL handlers, because
|
||||
* PROXY protocol header comes as the very first thing, even before SSL handshake request.
|
||||
*
|
||||
* @see <a href="https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt">The PROXY protocol</a>
|
||||
*/
|
||||
public class EppProxyProtocolHandler extends ByteToMessageDecoder {
|
||||
|
||||
/** Key used to retrieve origin IP address from a channel's attribute. */
|
||||
public static final AttributeKey<String> REMOTE_ADDRESS_KEY =
|
||||
AttributeKey.valueOf("REMOTE_ADDRESS_KEY");
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
// The proxy header must start with this prefix.
|
||||
// Sample header: "PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535\r\n".
|
||||
private static final byte[] HEADER_PREFIX = "PROXY".getBytes(US_ASCII);
|
||||
|
||||
private boolean finished = false;
|
||||
private String proxyHeader = null;
|
||||
|
||||
@Inject
|
||||
EppProxyProtocolHandler() {}
|
||||
|
||||
@Override
|
||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||
super.channelRead(ctx, msg);
|
||||
if (finished) {
|
||||
String remoteIP;
|
||||
if (proxyHeader != null) {
|
||||
logger.atFine().log("PROXIED CONNECTION: %s", ctx.channel());
|
||||
logger.atFine().log("PROXY HEADER for channel %s: %s", ctx.channel(), proxyHeader);
|
||||
String[] headerArray = proxyHeader.split(" ", -1);
|
||||
if (headerArray.length == 6) {
|
||||
String parsedIP = headerArray[2];
|
||||
if (InetAddresses.isInetAddress(parsedIP)) {
|
||||
remoteIP = parsedIP;
|
||||
logger.atFine().log(
|
||||
"Header parsed, using %s as remote IP for channel %s", remoteIP, ctx.channel());
|
||||
} else {
|
||||
logger.atWarning().log(
|
||||
"Invalid IP address in PROXY header: %s, falling back to source IP for channel %s",
|
||||
parsedIP, ctx.channel());
|
||||
remoteIP = getSourceIP(ctx);
|
||||
}
|
||||
// If the header is "PROXY UNKNOWN"
|
||||
// (see https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt), likely when the
|
||||
// remote connection to the external load balancer is through special means, make it
|
||||
// 0.0.0.0 so that it can be treated accordingly by the relevant quota configs.
|
||||
} else if (headerArray.length == 2 && headerArray[1].equals("UNKNOWN")) {
|
||||
logger.atFine().log(
|
||||
"Header parsed, source IP unknown, using 0.0.0.0 as remote IP for channel %s",
|
||||
ctx.channel());
|
||||
remoteIP = "0.0.0.0";
|
||||
} else {
|
||||
logger.atFine().log(
|
||||
"Cannot parse the header, using source IP as remote IP for channel %s",
|
||||
ctx.channel());
|
||||
remoteIP = getSourceIP(ctx);
|
||||
}
|
||||
} else {
|
||||
logger.atFine().log(
|
||||
"No header present, using source IP directly for channel %s", ctx.channel());
|
||||
remoteIP = getSourceIP(ctx);
|
||||
}
|
||||
if (remoteIP != null) {
|
||||
ctx.channel().attr(REMOTE_ADDRESS_KEY).set(remoteIP);
|
||||
} else {
|
||||
logger.atWarning().log("Not able to obtain remote IP for channel %s", ctx.channel());
|
||||
}
|
||||
// ByteToMessageDecoder automatically flushes unread bytes in the ByteBuf to the next handler
|
||||
// when itself is being removed.
|
||||
ctx.pipeline().remove(this);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getSourceIP(ChannelHandlerContext ctx) {
|
||||
SocketAddress remoteAddress = ctx.channel().remoteAddress();
|
||||
return (remoteAddress instanceof InetSocketAddress inetSocketAddress)
|
||||
? inetSocketAddress.getAddress().getHostAddress()
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to decode an internally accumulated buffer and find the proxy protocol header.
|
||||
*
|
||||
* <p>When the connection is not proxied (i. e. the initial bytes are not "PROXY"), simply set
|
||||
* {@link #finished} to true and allow the handler to be removed. Otherwise the handler waits
|
||||
* until there's enough bytes to parse the header, save the parsed header to {@link #proxyHeader},
|
||||
* and then mark {@link #finished}.
|
||||
*
|
||||
* @param in internally accumulated buffer, newly arrived bytes are appended to it.
|
||||
* @param out objects passed to the next handler, in this case nothing is ever passed because the
|
||||
* header itself is processed and written to the attribute of the proxy, and the handler is
|
||||
* then removed from the pipeline.
|
||||
*/
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
|
||||
// Wait until there are more bytes available than the header's length before processing.
|
||||
if (in.readableBytes() >= HEADER_PREFIX.length) {
|
||||
if (containsHeader(in)) {
|
||||
// The inbound message contains the header, it must be a proxied connection. Note that
|
||||
// currently proxied connection is only used for EPP protocol, which requires the connection
|
||||
// to be SSL enabled. So the beginning of the inbound message upon connection can only be
|
||||
// either the proxy header (when proxied), or SSL handshake request (when not proxied),
|
||||
// which does not start with "PROXY". Therefore it is safe to assume that if the beginning
|
||||
// of the message contains "PROXY", it must be proxied, and must contain \r\n.
|
||||
int eol = findEndOfLine(in);
|
||||
// If eol is not found, that is because that we do not yet have enough inbound message, do
|
||||
// nothing and wait for more bytes to be readable. eol will eventually be positive because
|
||||
// of the reasoning above: The connection starts with "PROXY", so it must be a proxied
|
||||
// connection and contain \r\n.
|
||||
if (eol >= 0) {
|
||||
// ByteBuf.readBytes is called so that the header is processed and not passed to handlers
|
||||
// further in the pipeline.
|
||||
byte[] headerBytes = new byte[eol];
|
||||
in.readBytes(headerBytes);
|
||||
proxyHeader = new String(headerBytes, US_ASCII);
|
||||
// Skip \r\n.
|
||||
in.skipBytes(2);
|
||||
// Proxy header processed, mark finished so that this handler is removed.
|
||||
finished = true;
|
||||
}
|
||||
} else {
|
||||
// The inbound message does not contain a proxy header, mark finished so that this handler
|
||||
// is removed. Note that no inbound bytes are actually processed by this handler because we
|
||||
// did not call ByteBuf.readBytes(), but ByteBuf.getByte(), which does not change reader
|
||||
// index of the ByteBuf. So any inbound byte is then passed to the next handler to process.
|
||||
finished = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index in the buffer of the end of line found. Returns -1 if no end of line was
|
||||
* found in the buffer.
|
||||
*/
|
||||
private static int findEndOfLine(final ByteBuf buffer) {
|
||||
final int n = buffer.writerIndex();
|
||||
for (int i = buffer.readerIndex(); i < n; i++) {
|
||||
final byte b = buffer.getByte(i);
|
||||
if (b == '\r' && i < n - 1 && buffer.getByte(i + 1) == '\n') {
|
||||
return i; // \r\n
|
||||
}
|
||||
}
|
||||
return -1; // Not found.
|
||||
}
|
||||
|
||||
/** Checks if the given buffer contains the proxy header prefix. */
|
||||
private boolean containsHeader(ByteBuf buffer) {
|
||||
// The readable bytes is always more or equal to the size of the header prefix because this
|
||||
// method is only called when this condition is true.
|
||||
checkState(buffer.readableBytes() >= HEADER_PREFIX.length);
|
||||
for (int i = 0; i < HEADER_PREFIX.length; ++i) {
|
||||
if (buffer.getByte(buffer.readerIndex() + i) != HEADER_PREFIX[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
package google.registry.eppserver.handler;
|
||||
|
||||
import static google.registry.eppserver.handler.EppProxyProtocolHandler.REMOTE_ADDRESS_KEY;
|
||||
import static google.registry.networking.handler.SslServerInitializer.CLIENT_CERTIFICATE_PROMISE_KEY;
|
||||
import static google.registry.util.GcpJsonFormatter.setCurrentRequest;
|
||||
import static google.registry.util.GcpJsonFormatter.setCurrentTraceId;
|
||||
@@ -45,6 +44,8 @@ import io.netty.util.concurrent.Promise;
|
||||
import io.netty.util.concurrent.ScheduledFuture;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -69,6 +70,9 @@ public class EppServiceHandler extends SimpleChannelInboundHandler<ByteBuf> {
|
||||
public static final AttributeKey<String> CLIENT_CERTIFICATE_HASH_KEY =
|
||||
AttributeKey.valueOf("CLIENT_CERTIFICATE_HASH_KEY");
|
||||
|
||||
public static final AttributeKey<String> REMOTE_ADDRESS_KEY =
|
||||
AttributeKey.valueOf("REMOTE_ADDRESS_KEY");
|
||||
|
||||
private final byte[] helloBytes;
|
||||
private final FrontendMetrics metrics;
|
||||
private final LocalConnectionLimiter localConnectionLimiter;
|
||||
@@ -128,8 +132,22 @@ public class EppServiceHandler extends SimpleChannelInboundHandler<ByteBuf> {
|
||||
if (!ctx.channel().isActive()) {
|
||||
return;
|
||||
}
|
||||
sslClientCertificateHash = getCertificateHash(cert);
|
||||
|
||||
clientAddress = ctx.channel().attr(REMOTE_ADDRESS_KEY).get();
|
||||
if (clientAddress == null) {
|
||||
SocketAddress remoteAddress = ctx.channel().remoteAddress();
|
||||
if (remoteAddress instanceof InetSocketAddress inetSocketAddress) {
|
||||
clientAddress = inetSocketAddress.getAddress().getHostAddress();
|
||||
}
|
||||
}
|
||||
|
||||
if (clientAddress == null || clientAddress.isEmpty()) {
|
||||
logger.atSevere().log("Failed to resolve client IP address, closing connection");
|
||||
closeConnection(ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
sslClientCertificateHash = getCertificateHash(cert);
|
||||
ctx.channel().attr(CLIENT_CERTIFICATE_HASH_KEY).set(sslClientCertificateHash);
|
||||
|
||||
// 1. Connection throttling (IP only pre-login)
|
||||
|
||||
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.Lists.partition;
|
||||
import static google.registry.util.ResourceUtils.readResourceUtf8;
|
||||
import static java.util.concurrent.Executors.newFixedThreadPool;
|
||||
|
||||
import com.google.cloud.tasks.v2.Task;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -39,11 +40,15 @@ import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Simple load test action that can generate configurable QPSes of various EPP actions.
|
||||
*
|
||||
* <p>This is not an end-to-end test. It exercises the Nomulus EPP service and the database, but
|
||||
* does not cover the proxy.
|
||||
*
|
||||
* <p>All aspects of the load test are configured via URL parameters that are specified when the
|
||||
* loadtest URL is being POSTed to. The {@code clientId} and {@code tld} parameters are required.
|
||||
* All of the other parameters are optional, but if none are specified then no actual load testing
|
||||
@@ -60,7 +65,7 @@ public class LoadTestAction implements Runnable {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private static final int NUM_QUEUES = 10;
|
||||
private static final int NUM_QUEUES = 20;
|
||||
private static final int MAX_TASKS_PER_LOAD = 100;
|
||||
private static final int ARBITRARY_VALID_HOST_LENGTH = 40;
|
||||
private static final int MAX_DOMAIN_LABEL_LENGTH = 63;
|
||||
@@ -72,20 +77,17 @@ public class LoadTestAction implements Runnable {
|
||||
|
||||
public static final String PATH = "/_dr/loadtest";
|
||||
|
||||
// Average task insertion rate with a dedicated thread enqueuing to one queue. This is used to
|
||||
// calculate the EPP request dispatch time. This value is based on observation and needs not to
|
||||
// be accurate. However, it should be low enough so that all EPP tasks are enqueued before the
|
||||
// first task is dispatched.
|
||||
private static final int TASK_INSERTIONS_PER_QUEUE_PER_MINUTE = 1000;
|
||||
|
||||
/** The ID of the registrar to use for load testing. */
|
||||
@Inject
|
||||
@Parameter("loadtestClientId")
|
||||
String registrarId;
|
||||
|
||||
/**
|
||||
* The number of seconds to delay the execution of the first load testing tasks by. Preparatory
|
||||
* work of creating independent hosts that will be used for later domain creation testing occurs
|
||||
* during this period, so make sure that it is long enough.
|
||||
*/
|
||||
@Inject
|
||||
@Parameter("delaySeconds")
|
||||
int delaySeconds;
|
||||
|
||||
/**
|
||||
* The number of seconds that tasks will be enqueued for. Note that if system QPS cannot handle
|
||||
* the given load then it will take longer than this number of seconds for the test to complete.
|
||||
@@ -157,9 +159,25 @@ public class LoadTestAction implements Runnable {
|
||||
xmlHostInfo = loadXml("host_info").replace("%host%", EXISTING_HOST);
|
||||
}
|
||||
|
||||
private int eppTaskCount() {
|
||||
// See `run()` below for details: summing two task-generating loops.
|
||||
return successfulDomainCreatesPerSecond
|
||||
+ runSeconds
|
||||
* (successfulHostCreatesPerSecond
|
||||
+ failedHostCreatesPerSecond
|
||||
+ domainInfosPerSecond
|
||||
+ domainChecksPerSecond
|
||||
+ hostInfosPerSecond
|
||||
+ successfulDomainCreatesPerSecond
|
||||
+ failedDomainCreatesPerSecond);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
validateAndLogRequest();
|
||||
// Delay the EPP request dispatch time to account for queue-insertion time.
|
||||
int delaySeconds =
|
||||
Math.ceilDiv(eppTaskCount(), TASK_INSERTIONS_PER_QUEUE_PER_MINUTE * NUM_QUEUES) * 60;
|
||||
validateAndLogRequest(delaySeconds);
|
||||
Instant initialStartSecond = clock.now().plus(Duration.ofSeconds(delaySeconds));
|
||||
ImmutableList.Builder<String> preTaskXmls = new ImmutableList.Builder<>();
|
||||
ImmutableList.Builder<String> hostPrefixesBuilder = new ImmutableList.Builder<>();
|
||||
@@ -209,7 +227,7 @@ public class LoadTestAction implements Runnable {
|
||||
logger.atInfo().log("Added %d total load test tasks.", taskOptions.size());
|
||||
}
|
||||
|
||||
private void validateAndLogRequest() {
|
||||
private void validateAndLogRequest(int delaySeconds) {
|
||||
checkArgument(
|
||||
RegistryEnvironment.get() != RegistryEnvironment.PRODUCTION,
|
||||
"DO NOT RUN LOADTESTS IN PROD!");
|
||||
@@ -297,9 +315,21 @@ public class LoadTestAction implements Runnable {
|
||||
|
||||
private void enqueue(ImmutableList<Task> tasks) {
|
||||
List<List<Task>> chunks = partition(tasks, MAX_TASKS_PER_LOAD);
|
||||
// Farm out tasks to multiple queues to work around queue qps quotas.
|
||||
for (int i = 0; i < chunks.size(); i++) {
|
||||
cloudTasksUtils.enqueue("load" + (i % NUM_QUEUES), chunks.get(i));
|
||||
// Farm out tasks to multiple queues to work around queue qps quotas. Use multiple threads to
|
||||
// speed up the enqueuing.
|
||||
try (ExecutorService executorService = newFixedThreadPool(NUM_QUEUES)) {
|
||||
for (int i = 0; i < chunks.size(); i++) {
|
||||
final int index = i;
|
||||
// Ignore `Future` returned by the pool b/c individual failures do not affect analysis.
|
||||
// lgtm[java/local-variable-is-never-read] Suppress Github CodeQL's outdated warning
|
||||
var _ =
|
||||
executorService.submit(
|
||||
() -> cloudTasksUtils.enqueue(getQueueName(index % NUM_QUEUES), chunks.get(index)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String getQueueName(int queueId) {
|
||||
return String.format("load%d", queueId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,12 +38,6 @@ public final class LoadTestModule {
|
||||
return extractRequiredParameter(req, "clientId");
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("delaySeconds")
|
||||
static int provideDelaySeconds(HttpServletRequest req) {
|
||||
return extractOptionalIntParameter(req, "delaySeconds").orElse(60);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("runSeconds")
|
||||
static int provideRunSeconds(HttpServletRequest req) {
|
||||
@@ -74,24 +68,6 @@ public final class LoadTestModule {
|
||||
return extractOptionalIntParameter(req, "domainChecks").orElse(0);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("successfulContactCreates")
|
||||
static int provideSuccessfulContactCreates(HttpServletRequest req) {
|
||||
return extractOptionalIntParameter(req, "successfulContactCreates").orElse(0);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("failedContactCreates")
|
||||
static int provideFailedContactCreates(HttpServletRequest req) {
|
||||
return extractOptionalIntParameter(req, "failedContactCreates").orElse(0);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("contactInfos")
|
||||
static int provideContactInfos(HttpServletRequest req) {
|
||||
return extractOptionalIntParameter(req, "contactInfos").orElse(0);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("successfulHostCreates")
|
||||
static int provideSuccessfulHostCreates(HttpServletRequest req) {
|
||||
|
||||
@@ -59,10 +59,10 @@ public final class PremiumList extends BaseDomainLabelList<BigDecimal, PremiumEn
|
||||
* Mapping from unqualified domain names to their prices.
|
||||
*
|
||||
* <p>This field requires special treatment since we want to lazy load it. We have to remove it
|
||||
* from the immutability contract so we can modify it after construction and we have to handle the
|
||||
* database processing on our own so we can detach it after load.
|
||||
* from the immutability contract so we can modify it after construction, and we have to handle
|
||||
* the database processing on our own so we can detach it after load.
|
||||
*/
|
||||
@Insignificant @Transient ImmutableMap<String, BigDecimal> labelsToPrices;
|
||||
@Insignificant @Transient volatile ImmutableMap<String, BigDecimal> labelsToPrices;
|
||||
|
||||
@Column(nullable = false)
|
||||
BloomFilter<String> bloomFilter;
|
||||
@@ -76,18 +76,27 @@ public final class PremiumList extends BaseDomainLabelList<BigDecimal, PremiumEn
|
||||
* Returns a {@link Map} of domain labels to prices.
|
||||
*
|
||||
* <p>Note that this is lazily loaded and thus must be called inside a transaction. You generally
|
||||
* should not be using this anyway as it's inefficient to load all of the PremiumEntry rows if you
|
||||
* should not be using this anyway as it's inefficient to load all the PremiumEntry rows if you
|
||||
* don't need them. To check prices, use {@link PremiumListDao#getPremiumPrice} instead.
|
||||
*
|
||||
* <p>We use locking to memoize the resulting object. We cannot use a simple memoizing Supplier
|
||||
* because we need to be able to set this value when creating the lists.
|
||||
*/
|
||||
public synchronized ImmutableMap<String, BigDecimal> getLabelsToPrices() {
|
||||
public ImmutableMap<String, BigDecimal> getLabelsToPrices() {
|
||||
if (labelsToPrices == null) {
|
||||
labelsToPrices =
|
||||
PremiumListDao.loadAllPremiumEntries(name).stream()
|
||||
.collect(
|
||||
toImmutableMap(
|
||||
PremiumEntry::getDomainLabel,
|
||||
// Set the correct amount of precision for the premium list's currency.
|
||||
premiumEntry -> convertAmountToMoney(premiumEntry.getValue()).getAmount()));
|
||||
synchronized (this) {
|
||||
// Extra null check to avoid race conditions
|
||||
if (labelsToPrices == null) {
|
||||
labelsToPrices =
|
||||
PremiumListDao.loadAllPremiumEntries(name).stream()
|
||||
.collect(
|
||||
toImmutableMap(
|
||||
PremiumEntry::getDomainLabel,
|
||||
// Set the correct amount of precision for the list's currency.
|
||||
premiumEntry ->
|
||||
convertAmountToMoney(premiumEntry.getValue()).getAmount()));
|
||||
}
|
||||
}
|
||||
}
|
||||
return labelsToPrices;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import static google.registry.config.RegistryConfig.getDomainLabelListCacheDurat
|
||||
import static google.registry.model.tld.label.ReservationType.FULLY_BLOCKED;
|
||||
import static google.registry.persistence.transaction.QueryComposer.Comparator.EQ;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.CollectionUtils.nullToEmpty;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import com.google.common.base.Splitter;
|
||||
@@ -69,7 +68,7 @@ public final class ReservedList
|
||||
* from the immutability contract so we can modify it after construction and we have to handle the
|
||||
* database processing on our own so we can detach it after load.
|
||||
*/
|
||||
@Insignificant @Transient Map<String, ReservedListEntry> reservedListMap;
|
||||
@Insignificant @Transient volatile ImmutableMap<String, ReservedListEntry> reservedListMap;
|
||||
|
||||
@RecursivePreRemove
|
||||
void preRemove() {
|
||||
@@ -149,7 +148,7 @@ public final class ReservedList
|
||||
}
|
||||
|
||||
/** A builder for constructing {@link ReservedListEntry} objects, since they are immutable. */
|
||||
private static class Builder
|
||||
public static class Builder
|
||||
extends DomainLabelEntry.Builder<ReservedListEntry, ReservedListEntry.Builder> {
|
||||
|
||||
Builder() {}
|
||||
@@ -185,19 +184,27 @@ public final class ReservedList
|
||||
*
|
||||
* <p>Note that this involves a database fetch of a potentially large number of elements and
|
||||
* should be avoided unless necessary.
|
||||
*
|
||||
* <p>We use locking to memoize the resulting object. We cannot use a simple memoizing Supplier
|
||||
* because we need to be able to set this value when creating the lists.
|
||||
*/
|
||||
public synchronized ImmutableMap<String, ReservedListEntry> getReservedListEntries() {
|
||||
public ImmutableMap<String, ReservedListEntry> getReservedListEntries() {
|
||||
if (reservedListMap == null) {
|
||||
reservedListMap =
|
||||
tm().reTransact(
|
||||
() ->
|
||||
tm()
|
||||
.createQueryComposer(ReservedListEntry.class)
|
||||
.where("revisionId", EQ, revisionId)
|
||||
.stream()
|
||||
.collect(toImmutableMap(ReservedListEntry::getDomainLabel, e -> e)));
|
||||
synchronized (this) {
|
||||
// Extra null check to avoid race conditions
|
||||
if (reservedListMap == null) {
|
||||
reservedListMap =
|
||||
tm().reTransact(
|
||||
() ->
|
||||
tm()
|
||||
.createQueryComposer(ReservedListEntry.class)
|
||||
.where("revisionId", EQ, revisionId)
|
||||
.stream()
|
||||
.collect(toImmutableMap(ReservedListEntry::getDomainLabel, e -> e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return ImmutableMap.copyOf(nullToEmpty(reservedListMap));
|
||||
return reservedListMap;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,7 +227,7 @@ public final class ReservedList
|
||||
*/
|
||||
public static ImmutableSet<ReservationType> getReservationTypes(String label, String tld) {
|
||||
checkNotNull(label, "label");
|
||||
if (label.length() == 0) {
|
||||
if (label.isEmpty()) {
|
||||
return ImmutableSet.of(FULLY_BLOCKED);
|
||||
}
|
||||
return getReservedListEntries(label, tld).stream()
|
||||
|
||||
@@ -271,7 +271,7 @@ public interface TransactionManager {
|
||||
* A runnable that allows for checked exceptions to be thrown.
|
||||
*
|
||||
* <p>This makes it easier to write lambdas without having to worry about wrapping and re-throwing
|
||||
* checked excpetions as unchecked ones.
|
||||
* checked exceptions as unchecked ones.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface ThrowingRunnable {
|
||||
|
||||
@@ -64,7 +64,7 @@ public final class NordnVerifyAction implements Runnable {
|
||||
static final String NORDN_URL_PARAM = "nordnUrl";
|
||||
static final String NORDN_LOG_ID_PARAM = "nordnLogId";
|
||||
|
||||
private static final String MARKSDB_URL_BEGINNING = "ry.marksdb.org";
|
||||
private static final String MARKSDB_HOST_NAME = "ry.marksdb.org";
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
@@ -109,11 +109,7 @@ public final class NordnVerifyAction implements Runnable {
|
||||
@VisibleForTesting
|
||||
LordnLog verify() throws IOException, GeneralSecurityException {
|
||||
String host = Ascii.toLowerCase(url.getHost());
|
||||
checkArgument(
|
||||
host.startsWith(MARKSDB_URL_BEGINNING),
|
||||
"URL %s must start with %s",
|
||||
url,
|
||||
MARKSDB_URL_BEGINNING);
|
||||
checkArgument(host.equals(MARKSDB_HOST_NAME), "Host %s must equal %s", host, MARKSDB_HOST_NAME);
|
||||
logger.atInfo().log("LORDN verify task %s: Sending request to URL %s", actionLogId, url);
|
||||
HttpURLConnection connection = urlConnectionService.createConnection(url);
|
||||
lordnRequestInitializer.initialize(connection, tld);
|
||||
|
||||
@@ -127,9 +127,7 @@ public final class TmchCertificateAuthority {
|
||||
* @see X509Utils#verifyCertificate
|
||||
*/
|
||||
public void verify(X509Certificate cert) throws GeneralSecurityException {
|
||||
synchronized (TmchCertificateAuthority.class) {
|
||||
X509Utils.verifyCertificate(getAndValidateRoot(), getCrl(), cert, clock.now());
|
||||
}
|
||||
X509Utils.verifyCertificate(getAndValidateRoot(), getCrl(), cert, clock.now());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,33 +155,23 @@ public final class TmchCertificateAuthority {
|
||||
}
|
||||
|
||||
public X509Certificate getAndValidateRoot() throws GeneralSecurityException {
|
||||
try {
|
||||
X509Certificate root = ROOT_CERTS.get(tmchCaMode);
|
||||
// The current production certificate expires on 2023-07-23. Future code monkey be reminded,
|
||||
// if you are looking at this code because the next line throws an exception, ask ICANN for a
|
||||
// new root certificate! (preferably before the current one expires...)
|
||||
root.checkValidity(Date.from(clock.now()));
|
||||
return root;
|
||||
} catch (Exception e) {
|
||||
if (e instanceof GeneralSecurityException generalSecurityException) {
|
||||
throw generalSecurityException;
|
||||
} else if (e instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
}
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
X509Certificate root = ROOT_CERTS.get(tmchCaMode);
|
||||
// The current production certificate expires on 2042-11-15. Future code monkey be reminded,
|
||||
// if you are looking at this code because the next line throws an exception, ask ICANN for a
|
||||
// new root certificate! (preferably before the current one expires...)
|
||||
root.checkValidity(Date.from(clock.now()));
|
||||
return root;
|
||||
}
|
||||
|
||||
public X509CRL getCrl() throws GeneralSecurityException {
|
||||
try {
|
||||
return CRL_CACHE.get(tmchCaMode);
|
||||
} catch (Exception e) {
|
||||
} catch (RuntimeException e) {
|
||||
if (e.getCause() instanceof GeneralSecurityException generalSecurityException) {
|
||||
throw generalSecurityException;
|
||||
} else if (e instanceof RuntimeException runtimeException) {
|
||||
throw runtimeException;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
package google.registry.batch;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static com.google.monitoring.metrics.contrib.LongMetricSubject.assertThat;
|
||||
import static google.registry.batch.SyncRemoteCacheAction.SyncStatus.FAILURE;
|
||||
import static google.registry.batch.SyncRemoteCacheAction.SyncStatus.NOT_CONFIGURED;
|
||||
import static google.registry.batch.SyncRemoteCacheAction.SyncStatus.SUCCESS;
|
||||
import static google.registry.model.common.Cursor.CursorType.REMOTE_CACHE_DOMAIN_SYNC;
|
||||
import static google.registry.model.common.Cursor.CursorType.REMOTE_CACHE_HOST_SYNC;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
@@ -73,15 +77,24 @@ class SyncRemoteCacheActionTest {
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
createTld("tld");
|
||||
SyncRemoteCacheAction.SYNC_CACHE_RUNS_METRIC.reset();
|
||||
action = new SyncRemoteCacheAction(lockHandler, response, Optional.of(jedisClient));
|
||||
}
|
||||
|
||||
private static void verifyMetrics(SyncRemoteCacheAction.SyncStatus status) {
|
||||
assertThat(SyncRemoteCacheAction.SYNC_CACHE_RUNS_METRIC)
|
||||
.hasValueForLabels(1, status.name())
|
||||
.and()
|
||||
.hasNoOtherValues();
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_noJedisConfig() {
|
||||
action = new SyncRemoteCacheAction(lockHandler, response, Optional.empty());
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
|
||||
assertThat(response.getPayload()).contains("No Jedis/Valkey configuration found");
|
||||
verifyMetrics(NOT_CONFIGURED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -91,6 +104,7 @@ class SyncRemoteCacheActionTest {
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_NO_CONTENT);
|
||||
assertThat(response.getPayload()).contains("Could not acquire lock");
|
||||
verifyMetrics(FAILURE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -100,6 +114,7 @@ class SyncRemoteCacheActionTest {
|
||||
action.run();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
|
||||
assertThat(response.getPayload()).contains("Errored out with cause");
|
||||
verifyMetrics(FAILURE);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -109,6 +124,7 @@ class SyncRemoteCacheActionTest {
|
||||
verifyNoInteractions(jedisClient);
|
||||
assertThat(DatabaseHelper.loadByKeyIfPresent(Cursor.createGlobalVKey(REMOTE_CACHE_DOMAIN_SYNC)))
|
||||
.isEmpty();
|
||||
verifyMetrics(SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -131,6 +147,7 @@ class SyncRemoteCacheActionTest {
|
||||
.getCursorTime()
|
||||
.toString())
|
||||
.isEqualTo("2025-01-01T00:00:00.001Z");
|
||||
verifyMetrics(SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -146,6 +163,7 @@ class SyncRemoteCacheActionTest {
|
||||
ImmutableList.of(
|
||||
new SimplifiedJedisClient.JedisResource<>("active.tld", activeDomain)));
|
||||
verify(jedisClient).deleteAll(Domain.class, ImmutableList.of("deleted.tld"));
|
||||
verifyMetrics(SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -166,6 +184,7 @@ class SyncRemoteCacheActionTest {
|
||||
verify(jedisClient)
|
||||
.setAll(
|
||||
ImmutableList.of(new SimplifiedJedisClient.JedisResource<>("example2.tld", domain2)));
|
||||
verifyMetrics(SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -175,6 +194,7 @@ class SyncRemoteCacheActionTest {
|
||||
verifyNoInteractions(jedisClient);
|
||||
assertThat(DatabaseHelper.loadByKeyIfPresent(Cursor.createGlobalVKey(REMOTE_CACHE_HOST_SYNC)))
|
||||
.isEmpty();
|
||||
verifyMetrics(SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -197,6 +217,7 @@ class SyncRemoteCacheActionTest {
|
||||
.getCursorTime()
|
||||
.toString())
|
||||
.isEqualTo("2025-01-01T00:00:00.001Z");
|
||||
verifyMetrics(SUCCESS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -212,5 +233,6 @@ class SyncRemoteCacheActionTest {
|
||||
ImmutableList.of(
|
||||
new SimplifiedJedisClient.JedisResource<>(active.getRepoId(), active)));
|
||||
verify(jedisClient).deleteAll(Host.class, ImmutableList.of(deleted.getRepoId()));
|
||||
verifyMetrics(SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT;
|
||||
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import google.registry.model.eppcommon.StatusValue;
|
||||
import google.registry.model.host.Host;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
@@ -52,7 +53,7 @@ public class RefreshDnsOnHostRenameActionTest {
|
||||
private RefreshDnsOnHostRenameAction action;
|
||||
|
||||
private void createAction(String hostKey) {
|
||||
action = new RefreshDnsOnHostRenameAction(hostKey, response);
|
||||
action = new RefreshDnsOnHostRenameAction(hostKey, response, clock);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
@@ -99,4 +100,28 @@ public class RefreshDnsOnHostRenameActionTest {
|
||||
assertThat(response.getPayload())
|
||||
.isEqualTo("Host to refresh is already deleted: ns1.example.tld");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_multipleBatches() {
|
||||
Host host = persistActiveHost("ns1.example.tld");
|
||||
ImmutableSet.Builder<String> domainNamesBuilder = new ImmutableSet.Builder<>();
|
||||
for (int i = 1; i <= 1001; i++) {
|
||||
String domainName = "example" + i + ".tld";
|
||||
domainNamesBuilder.add(domainName);
|
||||
persistResource(newDomain(domainName, host));
|
||||
}
|
||||
createAction(host.createVKey().stringify());
|
||||
action.run();
|
||||
assertDomainDnsRequests(Iterables.toArray(domainNamesBuilder.build(), String.class));
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSuccess_noLinkedDomains() {
|
||||
Host host = persistActiveHost("ns1.example.tld");
|
||||
createAction(host.createVKey().stringify());
|
||||
action.run();
|
||||
assertNoDnsRequests();
|
||||
assertThat(response.getStatus()).isEqualTo(SC_OK);
|
||||
}
|
||||
}
|
||||
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
// Copyright 2024 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.eppserver.handler;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class EppProxyProtocolHandlerTest {
|
||||
|
||||
private EmbeddedChannel createChannel(EppProxyProtocolHandler handler) {
|
||||
return new EmbeddedChannel(handler) {
|
||||
@Override
|
||||
public SocketAddress remoteAddress() {
|
||||
return new InetSocketAddress(InetAddress.getLoopbackAddress(), 12345);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProxyProtocol_parsesValidHeader() {
|
||||
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
|
||||
EmbeddedChannel channel = new EmbeddedChannel(handler);
|
||||
|
||||
String proxyHeader = "PROXY TCP4 192.168.1.1 10.0.0.1 50000 443\r\n";
|
||||
ByteBuf buffer = Unpooled.wrappedBuffer(proxyHeader.getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
channel.writeInbound(buffer);
|
||||
|
||||
String remoteAddress = channel.attr(EppProxyProtocolHandler.REMOTE_ADDRESS_KEY).get();
|
||||
assertThat(remoteAddress).isEqualTo("192.168.1.1");
|
||||
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProxyProtocol_invalidIP_fallsBackToSource() {
|
||||
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
|
||||
EmbeddedChannel channel = createChannel(handler);
|
||||
|
||||
String proxyHeader = "PROXY TCP4 invalid_ip_address 10.0.0.1 50000 443\r\n";
|
||||
ByteBuf buffer = Unpooled.wrappedBuffer(proxyHeader.getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
channel.writeInbound(buffer);
|
||||
|
||||
String remoteAddress = channel.attr(EppProxyProtocolHandler.REMOTE_ADDRESS_KEY).get();
|
||||
assertThat(remoteAddress).isEqualTo("127.0.0.1"); // Falls back to mocked remoteAddress
|
||||
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProxyProtocol_unknownHeader() {
|
||||
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
|
||||
EmbeddedChannel channel = new EmbeddedChannel(handler);
|
||||
|
||||
String proxyHeader = "PROXY UNKNOWN\r\n";
|
||||
ByteBuf buffer = Unpooled.wrappedBuffer(proxyHeader.getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
channel.writeInbound(buffer);
|
||||
|
||||
String remoteAddress = channel.attr(EppProxyProtocolHandler.REMOTE_ADDRESS_KEY).get();
|
||||
assertThat(remoteAddress).isEqualTo("0.0.0.0");
|
||||
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testProxyProtocol_noHeader_notProxied() {
|
||||
EppProxyProtocolHandler handler = new EppProxyProtocolHandler();
|
||||
EmbeddedChannel channel = createChannel(handler);
|
||||
|
||||
String normalData = "NOT_A_PROXY_HEADER";
|
||||
ByteBuf buffer = Unpooled.wrappedBuffer(normalData.getBytes(StandardCharsets.US_ASCII));
|
||||
|
||||
channel.writeInbound(buffer);
|
||||
|
||||
String remoteAddress = channel.attr(EppProxyProtocolHandler.REMOTE_ADDRESS_KEY).get();
|
||||
assertThat(remoteAddress).isEqualTo("127.0.0.1"); // Falls back to mocked remoteAddress
|
||||
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
|
||||
|
||||
ByteBuf passedOn = channel.readInbound();
|
||||
assertThat(passedOn.toString(StandardCharsets.US_ASCII)).isEqualTo("NOT_A_PROXY_HEADER");
|
||||
}
|
||||
}
|
||||
@@ -14,10 +14,10 @@
|
||||
|
||||
package google.registry.eppserver.handler;
|
||||
|
||||
import static google.registry.eppserver.handler.EppProxyProtocolHandler.REMOTE_ADDRESS_KEY;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.eppserver.handler.EppServiceHandler.REMOTE_ADDRESS_KEY;
|
||||
import static google.registry.networking.handler.SslServerInitializer.CLIENT_CERTIFICATE_PROMISE_KEY;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
@@ -51,6 +51,8 @@ import io.netty.util.concurrent.EventExecutor;
|
||||
import io.netty.util.concurrent.ImmediateEventExecutor;
|
||||
import io.netty.util.concurrent.Promise;
|
||||
import io.netty.util.concurrent.ScheduledFuture;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
@@ -382,7 +384,7 @@ class EppServiceHandlerTest {
|
||||
setUpSuccessfulHandshake();
|
||||
|
||||
Runnable timeoutTask = timeoutTaskCaptor.getValue();
|
||||
assertNotNull(timeoutTask);
|
||||
assertThat(timeoutTask).isNotNull();
|
||||
|
||||
ChannelFuture closeFuture = mock(ChannelFuture.class);
|
||||
when(ctx.close()).thenReturn(closeFuture);
|
||||
@@ -392,4 +394,69 @@ class EppServiceHandlerTest {
|
||||
verify(metrics).registerQuotaRejection("epp_login_timeout", "192.168.1.1");
|
||||
verify(ctx).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFallbackToSocketIpAddress_whenRemoteAddressKeyIsNull() throws Exception {
|
||||
certPromise = new DefaultPromise<>(ImmediateEventExecutor.INSTANCE);
|
||||
when(channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY)).thenReturn(certPromiseAttr);
|
||||
when(certPromiseAttr.get()).thenReturn(certPromise);
|
||||
|
||||
handler.channelActive(ctx);
|
||||
|
||||
// Mock REMOTE_ADDRESS_KEY to return null
|
||||
when(channel.attr(REMOTE_ADDRESS_KEY)).thenReturn(remoteAddressAttr);
|
||||
when(remoteAddressAttr.get()).thenReturn(null);
|
||||
|
||||
// Mock channel.remoteAddress() to return a socket address
|
||||
InetSocketAddress socketAddress = new InetSocketAddress("10.0.0.1", 12345);
|
||||
when(channel.remoteAddress()).thenReturn(socketAddress);
|
||||
|
||||
when(channel.attr(EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY)).thenReturn(certHashAttr);
|
||||
when(certificate.getEncoded()).thenReturn(new byte[] {1, 2, 3});
|
||||
|
||||
when(localConnectionLimiter.acquireIp("10.0.0.1")).thenReturn(true);
|
||||
when(idTokenSupplier.get()).thenReturn("fake_id_token");
|
||||
|
||||
// Stub request handler to capture the request
|
||||
ArgumentCaptor<FakeHttpServletRequest> requestCaptor =
|
||||
ArgumentCaptor.forClass(FakeHttpServletRequest.class);
|
||||
doAnswer(
|
||||
invocation -> {
|
||||
FakeHttpServletResponse rsp = invocation.getArgument(1);
|
||||
rsp.getWriter().write("<epp><greeting/></epp>");
|
||||
return null;
|
||||
})
|
||||
.when(requestHandler)
|
||||
.handleRequest(requestCaptor.capture(), any(FakeHttpServletResponse.class));
|
||||
|
||||
certPromise.setSuccess(certificate);
|
||||
|
||||
// Verify it resolved IP to 10.0.0.1
|
||||
verify(localConnectionLimiter).acquireIp("10.0.0.1");
|
||||
FakeHttpServletRequest capturedRequest = requestCaptor.getValue();
|
||||
assertThat(capturedRequest).isNotNull();
|
||||
assertThat(capturedRequest.getHeader(ProxyHttpHeaders.IP_ADDRESS)).isEqualTo("10.0.0.1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFallbackToSocketIpAddress_fails_closesConnection() throws Exception {
|
||||
certPromise = new DefaultPromise<>(ImmediateEventExecutor.INSTANCE);
|
||||
when(channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY)).thenReturn(certPromiseAttr);
|
||||
when(certPromiseAttr.get()).thenReturn(certPromise);
|
||||
|
||||
handler.channelActive(ctx);
|
||||
|
||||
// Mock REMOTE_ADDRESS_KEY to return null
|
||||
when(channel.attr(REMOTE_ADDRESS_KEY)).thenReturn(remoteAddressAttr);
|
||||
when(remoteAddressAttr.get()).thenReturn(null);
|
||||
|
||||
// Mock channel.remoteAddress() to return a non-InetSocketAddress (e.g. mock SocketAddress)
|
||||
SocketAddress mockSocketAddress = mock(SocketAddress.class);
|
||||
when(channel.remoteAddress()).thenReturn(mockSocketAddress);
|
||||
|
||||
certPromise.setSuccess(certificate);
|
||||
|
||||
// Verify it logged error and closed connection
|
||||
verify(ctx).close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,9 +170,7 @@ class NordnVerifyActionTest {
|
||||
void testFailure_badUrl() throws Exception {
|
||||
action.url = URI.create("http://example.com/blobio").toURL();
|
||||
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, action::run);
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("URL http://example.com/blobio must start with ry.marksdb.org");
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Host example.com must equal ry.marksdb.org");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -257,15 +257,15 @@ td.section {
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="property_name">generated by</td>
|
||||
<td class="property_value">SchemaCrawler 17.11.1</td>
|
||||
<td class="property_value">SchemaCrawler 17.12.2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="property_name">generated on</td>
|
||||
<td class="property_value">2026-07-14 19:18:20</td>
|
||||
<td class="property_value">2026-08-05 12:44:04</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="property_name">last flyway file</td>
|
||||
<td id="lastFlywayFile" class="property_value">V225__user_registry_lock_email_address_index.sql</td>
|
||||
<td id="lastFlywayFile" class="property_value">V226__tld_domain_name_index.sql</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -273,7 +273,7 @@ td.section {
|
||||
<p> </p>
|
||||
<svg viewBox="0.00 0.00 4783.00 3613.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="erDiagram" style="overflow: hidden; width: 100%; height: 800px">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 3608.5)">
|
||||
<title>SchemaCrawler_Diagram</title> <polygon fill="white" stroke="none" points="-4,4 -4,-3608.5 4778.75,-3608.5 4778.75,4 -4,4" /> <text xml:space="preserve" text-anchor="start" x="4535.5" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 17.11.1</text> <text xml:space="preserve" text-anchor="start" x="4534.75" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">2026-07-14 19:18:20</text> <polygon fill="none" stroke="#888888" points="4531.75,-4 4531.75,-45.5 4766.75,-45.5 4766.75,-4 4531.75,-4" /> <!-- allocationtoken_a08ccbef -->
|
||||
<title>SchemaCrawler_Diagram</title> <polygon fill="white" stroke="none" points="-4,4 -4,-3608.5 4778.75,-3608.5 4778.75,4 -4,4" /> <text xml:space="preserve" text-anchor="start" x="4535.5" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">generated by</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-29.2" font-family="Helvetica,sans-Serif" font-size="14.00">SchemaCrawler 17.12.2</text> <text xml:space="preserve" text-anchor="start" x="4534.75" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">generated on</text> <text xml:space="preserve" text-anchor="start" x="4618.25" y="-9.45" font-family="Helvetica,sans-Serif" font-size="14.00">2026-08-05 12:44:04</text> <polygon fill="none" stroke="#888888" points="4531.75,-4 4531.75,-45.5 4766.75,-45.5 4766.75,-4 4531.75,-4" /> <!-- allocationtoken_a08ccbef -->
|
||||
<g id="node1" class="node">
|
||||
<title>allocationtoken_a08ccbef</title> <polygon fill="#e9c2f2" stroke="none" points="479.25,-1014.62 479.25,-1034.38 664.25,-1034.38 664.25,-1014.62 479.25,-1014.62" /> <text xml:space="preserve" text-anchor="start" x="481.25" y="-1020.08" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">public."AllocationToken"</text> <polygon fill="#e9c2f2" stroke="none" points="664.25,-1014.62 664.25,-1034.38 737.25,-1034.38 737.25,-1014.62 664.25,-1014.62" /> <text xml:space="preserve" text-anchor="start" x="698.5" y="-1019.08" font-family="Helvetica,sans-Serif" font-size="14.00">[table]</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-1000.33" font-family="Helvetica,sans-Serif" font-weight="bold" font-style="italic" font-size="14.00">token</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-999.33" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-999.33" font-family="Helvetica,sans-Serif" font-size="14.00">text not null</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-979.58" font-family="Helvetica,sans-Serif" font-size="14.00">domain_name</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-979.58" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-979.58" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-959.83" font-family="Helvetica,sans-Serif" font-size="14.00">redemption_domain_repo_id</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-959.83" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-959.83" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <text xml:space="preserve" text-anchor="start" x="481.25" y="-940.08" font-family="Helvetica,sans-Serif" font-size="14.00">token_type</text> <text xml:space="preserve" text-anchor="start" x="658.5" y="-940.08" font-family="Helvetica,sans-Serif" font-size="14.00"> </text> <text xml:space="preserve" text-anchor="start" x="666.25" y="-940.08" font-family="Helvetica,sans-Serif" font-size="14.00">text</text> <polygon fill="none" stroke="#888888" points="478.25,-934.62 478.25,-1035.38 738.25,-1035.38 738.25,-934.62 478.25,-934.62" />
|
||||
</g>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -223,3 +223,4 @@ V222__remove_contact.sql
|
||||
V223__tld_change_xap_enabled_to_transitions.sql
|
||||
V224__add_registrar_expiry_access_period_enabled.sql
|
||||
V225__user_registry_lock_email_address_index.sql
|
||||
V226__tld_domain_name_index.sql
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- Copyright 2026 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.
|
||||
|
||||
-- RDAP queries by TLD need to order results. Using a btree that combines
|
||||
-- TLD and name allows us to do this quickly
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS domain_tld_domain_name_idx ON "Domain" (tld, domain_name);
|
||||
@@ -1841,6 +1841,13 @@ CREATE INDEX domain_history_to_transaction_record_idx ON public."DomainTransacti
|
||||
CREATE UNIQUE INDEX domain_no_duplicate_active ON public."Domain" USING btree (domain_name) WHERE (deletion_time = '294247-01-10 04:00:54.775+00'::timestamp with time zone);
|
||||
|
||||
|
||||
--
|
||||
-- Name: domain_tld_domain_name_idx; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
CREATE INDEX domain_tld_domain_name_idx ON public."Domain" USING btree (tld, domain_name);
|
||||
|
||||
|
||||
--
|
||||
-- Name: domaindsdatahistory_domain_history_revision_id_hash; Type: INDEX; Schema: public; Owner: -
|
||||
--
|
||||
|
||||
@@ -12,12 +12,13 @@ spec:
|
||||
metadata:
|
||||
labels:
|
||||
service: epp-server
|
||||
traffic: epp-all
|
||||
spec:
|
||||
serviceAccountName: nomulus
|
||||
nodeSelector:
|
||||
cloud.google.com/machine-family: c4
|
||||
containers:
|
||||
- name: frontend
|
||||
- name: epp-server
|
||||
image: gcr.io/GCP_PROJECT/nomulus
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
@@ -61,7 +62,7 @@ spec:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: CONTAINER_NAME
|
||||
value: frontend
|
||||
value: epp-server
|
||||
---
|
||||
apiVersion: autoscaling/v2
|
||||
kind: HorizontalPodAutoscaler
|
||||
@@ -98,7 +99,7 @@ spec:
|
||||
ipFamilies: [IPv4, IPv6]
|
||||
ipFamilyPolicy: RequireDualStack
|
||||
selector:
|
||||
service: epp-server
|
||||
traffic: epp-all
|
||||
ports:
|
||||
- port: 700
|
||||
targetPort: epp
|
||||
|
||||
@@ -84,6 +84,7 @@ steps:
|
||||
|
||||
sed -i "s|artifactStorage: artifactStorage|artifactStorage: $artifact_storage|" "$target_file"
|
||||
sed -i "s|serviceAccount: serviceAccount|serviceAccount: $service_account|" "$target_file"
|
||||
sed -i "s|serviceAccount: serviceAccount|serviceAccount: $service_account|" release/clouddeploy/delivery-pipeline.yaml
|
||||
sed -i "s|cluster: cluster|cluster: $cluster_val|" "$target_file"
|
||||
sed -i "s|workerPool: workerPool|workerPool: $worker_pool|" "$target_file"
|
||||
fi
|
||||
|
||||
@@ -76,6 +76,7 @@ steps:
|
||||
echo "$nomulus_digest" > /workspace/nomulus_digest
|
||||
proxy_digest=$(gcloud container images list-tags gcr.io/${PROJECT_ID}/proxy \
|
||||
--format="get(digest)" --filter="tags = ${TAG_NAME}")
|
||||
echo "$proxy_digest" > /workspace/proxy_digest
|
||||
gcloud --project=${PROJECT_ID} beta container binauthz attestations \
|
||||
sign-and-create --artifact-url=gcr.io/${PROJECT_ID}/nomulus@$nomulus_digest \
|
||||
--attestor=build-attestor --attestor-project=${PROJECT_ID} \
|
||||
@@ -196,14 +197,15 @@ steps:
|
||||
echo "============================================="
|
||||
# Read the pre-fetched image digest from the workspace file
|
||||
nomulus_digest=$(cat /workspace/nomulus_digest)
|
||||
proxy_digest=$(cat /workspace/proxy_digest)
|
||||
gcloud deploy releases create "$release_name" \
|
||||
--delivery-pipeline="$pipeline" \
|
||||
--region="$region" \
|
||||
--project=${PROJECT_ID} \
|
||||
--images="gcr.io/${PROJECT_ID}/nomulus=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest}" \
|
||||
--images="gcr.io/${PROJECT_ID}/nomulus=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},gcr.io/${PROJECT_ID}/proxy=gcr.io/${PROJECT_ID}/proxy@${proxy_digest}" \
|
||||
--source=. \
|
||||
--skaffold-file=release/clouddeploy/skaffold.yaml \
|
||||
--deploy-parameters="deployed_image=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},base_image=us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus"
|
||||
--deploy-parameters="deployed_image=gcr.io/${PROJECT_ID}/nomulus@${nomulus_digest},base_image=us-docker.pkg.dev/${PROJECT_ID}/gcr.io/nomulus,tag_name=${TAG_NAME},project_id=${PROJECT_ID}"
|
||||
# The tarballs and jars to upload to GCS.
|
||||
artifacts:
|
||||
objects:
|
||||
|
||||
@@ -236,11 +236,17 @@ steps:
|
||||
# partial phase manifests
|
||||
for stage in 1 5
|
||||
do
|
||||
if [ ${service} == backend ] || [ ${service} == console ]
|
||||
then
|
||||
replicas=1
|
||||
else
|
||||
replicas=${stage}
|
||||
fi
|
||||
awk 'NR==1,/^---$/ {if ($0 != "---") print}' ./jetty/kubernetes/nomulus-${env}-${service}.yaml | \
|
||||
sed s/name:\ ${service}/name:\ ${service}-partial-phase/g | \
|
||||
sed s/service:\ ${service}/deployment:\ ${service}-partial-phase/g | \
|
||||
sed s/value:\ ${service}/value:\ ${service}-partial-phase/g | \
|
||||
sed "/^spec:$/a\ replicas: ${stage}" \
|
||||
sed "/^spec:$/a\ replicas: ${replicas}" \
|
||||
> ./jetty/kubernetes/nomulus-${env}-${service}-partial-phase-${stage}.yaml
|
||||
done
|
||||
# gateway
|
||||
@@ -254,9 +260,20 @@ steps:
|
||||
> ./jetty/kubernetes/gateway/nomulus-backend-policy-${env}-${service}-canary.yaml
|
||||
done
|
||||
# Generate manifests for epp-server (which doesn't use HTTP gateway routing)
|
||||
# Lowercase EPP-v2 to epp-v2 to match reserved static GCE IP names.
|
||||
sed s/GCP_PROJECT/${PROJECT_ID}/g ./jetty/kubernetes/nomulus-epp-server.yaml | \
|
||||
sed s/latest/${TAG_NAME}/g | \
|
||||
sed s/ENVIRONMENT/${env}/g > ./jetty/kubernetes/nomulus-${env}-epp-server.yaml
|
||||
sed s/ENVIRONMENT/${env}/g | \
|
||||
sed s/EPP-v2/epp-v2/g > ./jetty/kubernetes/nomulus-${env}-epp-server.yaml
|
||||
# Generate partial-phase manifests for epp-server for Cloud Deploy canary stages
|
||||
for stage in 1 5
|
||||
do
|
||||
awk 'NR==1,/^---$/ {if ($0 != "---") print}' ./jetty/kubernetes/nomulus-${env}-epp-server.yaml | \
|
||||
sed s/name:\ epp-server/name:\ epp-server-partial-phase/g | \
|
||||
sed s/service:\ epp-server/deployment:\ epp-server-partial-phase/g | \
|
||||
sed "/^spec:$/a\ replicas: ${stage}" \
|
||||
> ./jetty/kubernetes/nomulus-${env}-epp-server-partial-phase-${stage}.yaml
|
||||
done
|
||||
done
|
||||
# Upload the Gradle binary to GCS if it does not exist and point URL in Gradle wrapper to it.
|
||||
- name: 'gcr.io/cloud-builders/gcloud'
|
||||
|
||||
@@ -5,7 +5,7 @@ This directory contains the Google Cloud Deploy configuration files for the Nomu
|
||||
## Files
|
||||
|
||||
### `delivery-pipeline.yaml`
|
||||
Defines the `DeliveryPipeline` resource named `deploy-nomulus`. It sets up the serial pipeline for rolling out changes to different targets.
|
||||
Defines the `DeliveryPipeline` resource named `deploy-nomulus` and its associated `Automation` resource (`deploy-nomulus/auto-advance-canary`). It sets up the serial pipeline for rolling out changes to different targets and automatically advancing canary rollouts.
|
||||
|
||||
### Target Configurations (e.g., `crash-target.yaml`)
|
||||
Files matching this format define the `Target` resources for Cloud Deploy. They specify the GKE cluster and other environment-specific settings for deployment.
|
||||
|
||||
@@ -14,17 +14,6 @@ serialPipeline:
|
||||
- phaseId: "canary-1"
|
||||
profiles: ["crash-partial-phase-1"]
|
||||
percentage: 10
|
||||
predeploy:
|
||||
tasks:
|
||||
- type: container
|
||||
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
|
||||
env:
|
||||
TARGET_ID: ${{ target.id }}
|
||||
command: ["/bin/bash"]
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
gcloud builds submit --config=release/cloudbuild-schema-verify-${TARGET_ID}.yaml
|
||||
analysis:
|
||||
# 10 minutes.
|
||||
duration: 600s
|
||||
@@ -55,17 +44,20 @@ serialPipeline:
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
gcloud artifacts docker tags add $DEPLOYED_IMAGE \
|
||||
${BASE_IMAGE}:live-cd-${TARGET_ID}
|
||||
gcloud container images add-tag $DEPLOYED_IMAGE \
|
||||
${BASE_IMAGE}:live-cd-${TARGET_ID} --quiet
|
||||
- type: container
|
||||
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
|
||||
env:
|
||||
TARGET_ID: ${{ target.id }}
|
||||
TAG_NAME: ${{ deploy_params['tag_name'] }}
|
||||
PROJECT_ID: ${{ deploy_params['project_id'] }}
|
||||
command: ["/bin/bash"]
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
gcloud builds submit --config=release/cloudbuild-schema-deploy-${TARGET_ID}.yaml
|
||||
gcloud storage cp gs://${PROJECT_ID}-deploy/${TAG_NAME}/cloudbuild-schema-deploy-${TARGET_ID}.yaml .
|
||||
gcloud builds submit --no-source --config=cloudbuild-schema-deploy-${TARGET_ID}.yaml
|
||||
analysis:
|
||||
# 10 minutes.
|
||||
duration: 600s
|
||||
@@ -86,11 +78,14 @@ serialPipeline:
|
||||
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
|
||||
env:
|
||||
TARGET_ID: ${{ target.id }}
|
||||
TAG_NAME: ${{ deploy_params['tag_name'] }}
|
||||
PROJECT_ID: ${{ deploy_params['project_id'] }}
|
||||
command: ["/bin/bash"]
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
gcloud builds submit --config=release/cloudbuild-schema-verify-${TARGET_ID}.yaml
|
||||
gcloud storage cp gs://${PROJECT_ID}-deploy/${TAG_NAME}/cloudbuild-schema-verify-${TARGET_ID}.yaml .
|
||||
gcloud builds submit --no-source --config=cloudbuild-schema-verify-${TARGET_ID}.yaml
|
||||
analysis:
|
||||
# 10 minutes.
|
||||
duration: 600s
|
||||
@@ -121,20 +116,42 @@ serialPipeline:
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
gcloud artifacts docker tags add $DEPLOYED_IMAGE \
|
||||
${BASE_IMAGE}:live-cd-${TARGET_ID}
|
||||
gcloud container images add-tag $DEPLOYED_IMAGE \
|
||||
${BASE_IMAGE}:live-cd-${TARGET_ID} --quiet
|
||||
- type: container
|
||||
image: gcr.io/google.com/cloudsdktool/google-cloud-cli:stable
|
||||
env:
|
||||
TARGET_ID: ${{ target.id }}
|
||||
TAG_NAME: ${{ deploy_params['tag_name'] }}
|
||||
PROJECT_ID: ${{ deploy_params['project_id'] }}
|
||||
command: ["/bin/bash"]
|
||||
args:
|
||||
- "-c"
|
||||
- |
|
||||
gcloud builds submit --config=release/cloudbuild-schema-deploy-${TARGET_ID}.yaml
|
||||
gcloud storage cp gs://${PROJECT_ID}-deploy/${TAG_NAME}/cloudbuild-schema-deploy-${TARGET_ID}.yaml .
|
||||
gcloud builds submit --no-source --config=cloudbuild-schema-deploy-${TARGET_ID}.yaml
|
||||
analysis:
|
||||
# 10 minutes.
|
||||
duration: 600s
|
||||
googleCloud:
|
||||
alertPolicyChecks:
|
||||
sandboxStableDeploymentAlertPolicyChecks
|
||||
---
|
||||
apiVersion: deploy.cloud.google.com/v1
|
||||
kind: Automation
|
||||
metadata:
|
||||
name: deploy-nomulus/auto-advance-canary
|
||||
description: Automatically advances rollouts through canary-1 phase after successful deployment and analysis.
|
||||
# Placeholder: Replace with project service account.
|
||||
serviceAccount: serviceAccount
|
||||
selector:
|
||||
targets:
|
||||
- id: crash
|
||||
- id: sandbox
|
||||
rules:
|
||||
- advanceRolloutRule:
|
||||
id: advance-canary-phases
|
||||
sourcePhases:
|
||||
- "canary-1"
|
||||
wait: 0m
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ profiles:
|
||||
- ../../jetty/kubernetes/nomulus-crash-console.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-frontend.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-pubapi.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-epp-server.yaml
|
||||
deploy:
|
||||
kubectl: { }
|
||||
- name: crash-partial-phase-1
|
||||
@@ -20,6 +21,7 @@ profiles:
|
||||
- ../../jetty/kubernetes/nomulus-crash-console-partial-phase-1.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-frontend-partial-phase-1.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-pubapi-partial-phase-1.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-epp-server-partial-phase-1.yaml
|
||||
deploy:
|
||||
kubectl: { }
|
||||
- name: crash-partial-phase-5
|
||||
@@ -29,6 +31,7 @@ profiles:
|
||||
- ../../jetty/kubernetes/nomulus-crash-console-partial-phase-5.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-frontend-partial-phase-5.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-pubapi-partial-phase-5.yaml
|
||||
- ../../jetty/kubernetes/nomulus-crash-epp-server-partial-phase-5.yaml
|
||||
deploy:
|
||||
kubectl: { }
|
||||
- name: sandbox
|
||||
@@ -38,6 +41,7 @@ profiles:
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-console.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-frontend.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-pubapi.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-epp-server.yaml
|
||||
deploy:
|
||||
kubectl: { }
|
||||
- name: sandbox-partial-phase-1
|
||||
@@ -47,6 +51,7 @@ profiles:
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-console-partial-phase-1.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-frontend-partial-phase-1.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-pubapi-partial-phase-1.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-epp-server-partial-phase-1.yaml
|
||||
deploy:
|
||||
kubectl: { }
|
||||
- name: sandbox-partial-phase-5
|
||||
@@ -56,5 +61,6 @@ profiles:
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-console-partial-phase-5.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-frontend-partial-phase-5.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-pubapi-partial-phase-5.yaml
|
||||
- ../../jetty/kubernetes/nomulus-sandbox-epp-server-partial-phase-5.yaml
|
||||
deploy:
|
||||
kubectl: { }
|
||||
Reference in New Issue
Block a user