mirror of
https://github.com/google/nomulus.git
synced 2025-05-29 00:40:09 +02:00
Add QuotaHandler to GCP proxy
The quota handler terminates connections when quota is exceeded. The next CL will add instrumentation for quota related metrics. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=185042675
This commit is contained in:
parent
d38e29fd5e
commit
6ca523386a
10 changed files with 550 additions and 35 deletions
|
@ -22,6 +22,7 @@ import com.google.common.base.Supplier;
|
|||
import com.google.common.base.Suppliers;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import dagger.Component;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
@ -31,6 +32,8 @@ import google.registry.proxy.HttpsRelayProtocolModule.HttpsRelayProtocol;
|
|||
import google.registry.proxy.WhoisProtocolModule.WhoisProtocol;
|
||||
import google.registry.proxy.handler.BackendMetricsHandler;
|
||||
import google.registry.proxy.handler.ProxyProtocolHandler;
|
||||
import google.registry.proxy.handler.QuotaHandler.EppQuotaHandler;
|
||||
import google.registry.proxy.handler.QuotaHandler.WhoisQuotaHandler;
|
||||
import google.registry.proxy.handler.RelayHandler.FullHttpRequestRelayHandler;
|
||||
import google.registry.proxy.handler.RelayHandler.FullHttpResponseRelayHandler;
|
||||
import google.registry.proxy.handler.SslClientInitializer;
|
||||
|
@ -47,6 +50,9 @@ import io.netty.handler.ssl.util.SelfSignedCertificate;
|
|||
import io.netty.handler.timeout.ReadTimeoutHandler;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import javax.inject.Named;
|
||||
|
@ -95,6 +101,9 @@ public abstract class ProtocolModuleTest {
|
|||
LoggingHandler.class,
|
||||
// Metrics instrumentation is tested separately.
|
||||
BackendMetricsHandler.class,
|
||||
// Quota management is tested separately.
|
||||
WhoisQuotaHandler.class,
|
||||
EppQuotaHandler.class,
|
||||
ReadTimeoutHandler.class);
|
||||
|
||||
protected EmbeddedChannel channel;
|
||||
|
@ -265,5 +274,17 @@ public abstract class ProtocolModuleTest {
|
|||
Clock provideFakeClock() {
|
||||
return fakeClock;
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
ExecutorService provideExecutorService() {
|
||||
return MoreExecutors.newDirectExecutorService();
|
||||
}
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
ScheduledExecutorService provideScheduledExecutorService() {
|
||||
return Executors.newSingleThreadScheduledExecutor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
137
javatests/google/registry/proxy/handler/EppQuotaHandlerTest.java
Normal file
137
javatests/google/registry/proxy/handler/EppQuotaHandlerTest.java
Normal file
|
@ -0,0 +1,137 @@
|
|||
// 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.proxy.handler;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.proxy.handler.EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY;
|
||||
import static google.registry.testing.JUnitBackports.expectThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import google.registry.proxy.handler.QuotaHandler.EppQuotaHandler;
|
||||
import google.registry.proxy.handler.QuotaHandler.OverQuotaException;
|
||||
import google.registry.proxy.quota.QuotaManager;
|
||||
import google.registry.proxy.quota.QuotaManager.QuotaRebate;
|
||||
import google.registry.proxy.quota.QuotaManager.QuotaRequest;
|
||||
import google.registry.proxy.quota.QuotaManager.QuotaResponse;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link EppQuotaHandler} */
|
||||
@RunWith(JUnit4.class)
|
||||
public class EppQuotaHandlerTest {
|
||||
|
||||
private final QuotaManager quotaManager = mock(QuotaManager.class);
|
||||
private final EppQuotaHandler handler = new EppQuotaHandler(quotaManager);
|
||||
private final EmbeddedChannel channel = new EmbeddedChannel(handler);
|
||||
private final String clientCertHash = "blah/123!";
|
||||
private final DateTime now = DateTime.now(DateTimeZone.UTC);
|
||||
private final Object message = new Object();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
channel.attr(CLIENT_CERTIFICATE_HASH_KEY).set(clientCertHash);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_quotaGrantedAndReturned() {
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(clientCertHash)))
|
||||
.thenReturn(QuotaResponse.create(true, clientCertHash, now));
|
||||
|
||||
// First read, acquire quota.
|
||||
assertThat(channel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(message);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
verify(quotaManager).acquireQuota(QuotaRequest.create(clientCertHash));
|
||||
|
||||
// Second read, should not acquire quota again.
|
||||
Object newMessage = new Object();
|
||||
assertThat(channel.writeInbound(newMessage)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(newMessage);
|
||||
verifyNoMoreInteractions(quotaManager);
|
||||
|
||||
// Channel closed, release quota.
|
||||
ChannelFuture unusedFuture = channel.close();
|
||||
verify(quotaManager)
|
||||
.releaseQuota(QuotaRebate.create(QuotaResponse.create(true, clientCertHash, now)));
|
||||
verifyNoMoreInteractions(quotaManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_quotaNotGranted() {
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(clientCertHash)))
|
||||
.thenReturn(QuotaResponse.create(false, clientCertHash, now));
|
||||
OverQuotaException e =
|
||||
expectThrows(OverQuotaException.class, () -> channel.writeInbound(message));
|
||||
assertThat(e).hasMessageThat().contains(clientCertHash);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_twoChannels_twoUserIds() {
|
||||
// Set up another user.
|
||||
final EppQuotaHandler otherHandler = new EppQuotaHandler(quotaManager);
|
||||
final EmbeddedChannel otherChannel = new EmbeddedChannel(otherHandler);
|
||||
final String otherClientCertHash = "hola@9x";
|
||||
otherChannel.attr(CLIENT_CERTIFICATE_HASH_KEY).set(otherClientCertHash);
|
||||
final DateTime later = now.plus(Duration.standardSeconds(1));
|
||||
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(clientCertHash)))
|
||||
.thenReturn(QuotaResponse.create(true, clientCertHash, now));
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(otherClientCertHash)))
|
||||
.thenReturn(QuotaResponse.create(false, otherClientCertHash, later));
|
||||
|
||||
// Allows the first user.
|
||||
assertThat(channel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(message);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
|
||||
// Blocks the second user.
|
||||
OverQuotaException e =
|
||||
expectThrows(OverQuotaException.class, () -> otherChannel.writeInbound(message));
|
||||
assertThat(e).hasMessageThat().contains(otherClientCertHash);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_twoChannels_sameUserIds() {
|
||||
// Set up another channel for the same user.
|
||||
final EppQuotaHandler otherHandler = new EppQuotaHandler(quotaManager);
|
||||
final EmbeddedChannel otherChannel = new EmbeddedChannel(otherHandler);
|
||||
otherChannel.attr(CLIENT_CERTIFICATE_HASH_KEY).set(clientCertHash);
|
||||
final DateTime later = now.plus(Duration.standardSeconds(1));
|
||||
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(clientCertHash)))
|
||||
.thenReturn(QuotaResponse.create(true, clientCertHash, now))
|
||||
.thenReturn(QuotaResponse.create(false, clientCertHash, later));
|
||||
|
||||
// Allows the first channel.
|
||||
assertThat(channel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(message);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
|
||||
// Blocks the second channel.
|
||||
OverQuotaException e =
|
||||
expectThrows(OverQuotaException.class, () -> otherChannel.writeInbound(message));
|
||||
assertThat(e).hasMessageThat().contains(clientCertHash);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
// 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.proxy.handler;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY;
|
||||
import static google.registry.testing.JUnitBackports.expectThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import google.registry.proxy.handler.QuotaHandler.OverQuotaException;
|
||||
import google.registry.proxy.handler.QuotaHandler.WhoisQuotaHandler;
|
||||
import google.registry.proxy.quota.QuotaManager;
|
||||
import google.registry.proxy.quota.QuotaManager.QuotaRequest;
|
||||
import google.registry.proxy.quota.QuotaManager.QuotaResponse;
|
||||
import io.netty.channel.ChannelFuture;
|
||||
import io.netty.channel.embedded.EmbeddedChannel;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.DateTimeZone;
|
||||
import org.joda.time.Duration;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
/** Unit tests for {@link WhoisQuotaHandler} */
|
||||
@RunWith(JUnit4.class)
|
||||
public class WhoisQuotaHandlerTest {
|
||||
|
||||
private final QuotaManager quotaManager = mock(QuotaManager.class);
|
||||
private final WhoisQuotaHandler handler = new WhoisQuotaHandler(quotaManager);
|
||||
private final EmbeddedChannel channel = new EmbeddedChannel(handler);
|
||||
private final DateTime now = DateTime.now(DateTimeZone.UTC);
|
||||
private final String remoteAddress = "127.0.0.1";
|
||||
private final Object message = new Object();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
channel.attr(REMOTE_ADDRESS_KEY).set(remoteAddress);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_quotaGranted() {
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(remoteAddress)))
|
||||
.thenReturn(QuotaResponse.create(true, remoteAddress, now));
|
||||
|
||||
// First read, acquire quota.
|
||||
assertThat(channel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(message);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
verify(quotaManager).acquireQuota(QuotaRequest.create(remoteAddress));
|
||||
|
||||
// Second read, should not acquire quota again.
|
||||
assertThat(channel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(message);
|
||||
|
||||
// Channel closed, release quota.
|
||||
ChannelFuture unusedFuture = channel.close();
|
||||
verifyNoMoreInteractions(quotaManager);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFailure_quotaNotGranted() {
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(remoteAddress)))
|
||||
.thenReturn(QuotaResponse.create(false, remoteAddress, now));
|
||||
OverQuotaException e =
|
||||
expectThrows(OverQuotaException.class, () -> channel.writeInbound(message));
|
||||
assertThat(e).hasMessageThat().contains("none");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_twoChannels_twoUserIds() {
|
||||
// Set up another user.
|
||||
final WhoisQuotaHandler otherHandler = new WhoisQuotaHandler(quotaManager);
|
||||
final EmbeddedChannel otherChannel = new EmbeddedChannel(otherHandler);
|
||||
final String otherRemoteAddress = "192.168.0.1";
|
||||
otherChannel.attr(REMOTE_ADDRESS_KEY).set(otherRemoteAddress);
|
||||
final DateTime later = now.plus(Duration.standardSeconds(1));
|
||||
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(remoteAddress)))
|
||||
.thenReturn(QuotaResponse.create(true, remoteAddress, now));
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(otherRemoteAddress)))
|
||||
.thenReturn(QuotaResponse.create(false, otherRemoteAddress, later));
|
||||
|
||||
// Allows the first user.
|
||||
assertThat(channel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(message);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
|
||||
// Blocks the second user.
|
||||
OverQuotaException e =
|
||||
expectThrows(OverQuotaException.class, () -> otherChannel.writeInbound(message));
|
||||
assertThat(e).hasMessageThat().contains("none");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSuccess_oneUser_rateLimited() {
|
||||
// Set up another channel for the same user.
|
||||
final WhoisQuotaHandler otherHandler = new WhoisQuotaHandler(quotaManager);
|
||||
final EmbeddedChannel otherChannel = new EmbeddedChannel(otherHandler);
|
||||
otherChannel.attr(REMOTE_ADDRESS_KEY).set(remoteAddress);
|
||||
final DateTime later = now.plus(Duration.standardSeconds(1));
|
||||
|
||||
// Set up the third channel for the same user
|
||||
final WhoisQuotaHandler thirdHandler = new WhoisQuotaHandler(quotaManager);
|
||||
final EmbeddedChannel thirdChannel = new EmbeddedChannel(thirdHandler);
|
||||
thirdChannel.attr(REMOTE_ADDRESS_KEY).set(remoteAddress);
|
||||
final DateTime evenLater = now.plus(Duration.standardSeconds(60));
|
||||
|
||||
when(quotaManager.acquireQuota(QuotaRequest.create(remoteAddress)))
|
||||
.thenReturn(QuotaResponse.create(true, remoteAddress, now))
|
||||
// Throttles the second connection.
|
||||
.thenReturn(QuotaResponse.create(false, remoteAddress, later))
|
||||
// Allows the third connection because token refilled.
|
||||
.thenReturn(QuotaResponse.create(true, remoteAddress, evenLater));
|
||||
|
||||
// Allows the first channel.
|
||||
assertThat(channel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) channel.readInbound()).isEqualTo(message);
|
||||
assertThat(channel.isActive()).isTrue();
|
||||
|
||||
// Blocks the second channel.
|
||||
OverQuotaException e =
|
||||
expectThrows(OverQuotaException.class, () -> otherChannel.writeInbound(message));
|
||||
assertThat(e).hasMessageThat().contains("none");
|
||||
|
||||
// Allows the third channel.
|
||||
assertThat(thirdChannel.writeInbound(message)).isTrue();
|
||||
assertThat((Object) thirdChannel.readInbound()).isEqualTo(message);
|
||||
assertThat(thirdChannel.isActive()).isTrue();
|
||||
}
|
||||
}
|
|
@ -15,8 +15,6 @@
|
|||
package google.registry.proxy.quota;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.proxy.quota.QuotaManager.QuotaResponse.Status.FAILURE;
|
||||
import static google.registry.proxy.quota.QuotaManager.QuotaResponse.Status.SUCCESS;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
@ -56,7 +54,7 @@ public class QuotaManagerTest {
|
|||
|
||||
request = QuotaRequest.create(USER_ID);
|
||||
response = quotaManager.acquireQuota(request);
|
||||
assertThat(response.status()).isEqualTo(SUCCESS);
|
||||
assertThat(response.success()).isTrue();
|
||||
assertThat(response.userId()).isEqualTo(USER_ID);
|
||||
assertThat(response.grantedTokenRefillTime()).isEqualTo(clock.nowUtc());
|
||||
}
|
||||
|
@ -67,7 +65,7 @@ public class QuotaManagerTest {
|
|||
|
||||
request = QuotaRequest.create(USER_ID);
|
||||
response = quotaManager.acquireQuota(request);
|
||||
assertThat(response.status()).isEqualTo(FAILURE);
|
||||
assertThat(response.success()).isFalse();
|
||||
assertThat(response.userId()).isEqualTo(USER_ID);
|
||||
assertThat(response.grantedTokenRefillTime()).isEqualTo(clock.nowUtc());
|
||||
}
|
||||
|
@ -75,7 +73,7 @@ public class QuotaManagerTest {
|
|||
@Test
|
||||
public void testSuccess_rebate() throws Exception {
|
||||
DateTime grantedTokenRefillTime = clock.nowUtc();
|
||||
response = QuotaResponse.create(SUCCESS, USER_ID, grantedTokenRefillTime);
|
||||
response = QuotaResponse.create(true, USER_ID, grantedTokenRefillTime);
|
||||
rebate = QuotaRebate.create(response);
|
||||
Future<?> unusedFuture = quotaManager.releaseQuota(rebate);
|
||||
verify(tokenStore).scheduleRefresh();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue