Migrate external EPP proxy to integrated Nomulus EPP server (#3126)

* Remove proxy, add new service

* Consolidate MetricReporter under single listener
This commit is contained in:
Pavlo Tkach
2026-07-21 19:04:33 +00:00
committed by GitHub
parent b1c6e07fa2
commit b187193e79
45 changed files with 3871 additions and 224 deletions
-123
View File
@@ -628,18 +628,6 @@
}
}
},
"node_modules/@angular/build/node_modules/@types/node": {
"version": "25.9.1",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/@types/node/-/node-25.9.1.tgz",
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": ">=7.24.0 <7.24.7"
}
},
"node_modules/@angular/build/node_modules/@vitejs/plugin-basic-ssl": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.1.0.tgz",
@@ -653,24 +641,6 @@
"vite": "^6.0.0 || ^7.0.0"
}
},
"node_modules/@angular/build/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/build/node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -694,22 +664,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@angular/build/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/build/node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
@@ -743,15 +697,6 @@
"node": ">= 12"
}
},
"node_modules/@angular/build/node_modules/undici-types": {
"version": "7.24.6",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/undici-types/-/undici-types-7.24.6.tgz",
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@angular/build/node_modules/vite": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
@@ -971,24 +916,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@angular/cli/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/cli/node_modules/cli-spinners": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz",
@@ -1092,22 +1019,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@angular/cli/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@angular/cli/node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
@@ -4695,24 +4606,6 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@schematics/angular/node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/chokidar/-/chokidar-5.0.0.tgz",
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"readdirp": "^5.0.0"
},
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@schematics/angular/node_modules/cli-spinners": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz",
@@ -4816,22 +4709,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/@schematics/angular/node_modules/readdirp": {
"version": "5.0.0",
"resolved": "https://us-npm.pkg.dev/artifact-foundry-prod/ah-3p-staging-npm/readdirp/-/readdirp-5.0.0.tgz",
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"engines": {
"node": ">= 20.19.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@schematics/angular/node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+12
View File
@@ -126,6 +126,7 @@ dependencies {
implementation deps['com.google.api:gax']
implementation deps['com.google.api.grpc:proto-google-common-protos']
implementation deps['com.google.api.grpc:proto-google-cloud-secretmanager-v1']
implementation deps['com.google.apis:google-api-services-cloudkms']
implementation deps['com.google.api-client:google-api-client']
implementation deps['com.google.api-client:google-api-client-servlet']
implementation deps['com.google.monitoring-client:metrics']
@@ -244,6 +245,7 @@ dependencies {
implementation project(':common')
testImplementation project(path: ':common', configuration: 'testing')
implementation project(':util')
implementation project(':networking')
// Import NomulusPostreSql from ':db' for implementation but exclude dependencies.
implementation project(path: ':db', configuration: 'implementationApi')
nonprodRuntimeOnly project(':db')
@@ -277,6 +279,16 @@ dependencies {
testImplementation deps['org.mockito:mockito-core']
testImplementation deps['org.mockito:mockito-junit-jupiter']
implementation deps['io.netty:netty-buffer']
implementation deps['io.netty:netty-codec-http']
implementation deps['io.netty:netty-codec']
implementation deps['io.netty:netty-common']
implementation deps['io.netty:netty-handler']
implementation deps['io.netty:netty-transport']
implementation deps['redis.clients:jedis']
runtimeOnly deps['io.netty:netty-tcnative-boringssl-static']
implementation deps['org.yaml:snakeyaml']
// Indirect dependency found by undeclared-dependency check. Such
// dependencies should go after all other implementation and testImplementation
// dependencies to avoid overriding them accidentally.
+24 -19
View File
@@ -86,10 +86,11 @@ com.google.api:gax-httpjson:2.82.0=compileClasspath,deploy_jar,nonprodCompileCla
com.google.api:gax:2.82.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-admin-directory:directory_v1-rev20260531-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-bigquery:v2-rev20251012-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-cloudkms:v1-rev20260702-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-cloudresourcemanager:v1-rev20250606-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-dataflow:v1b3-rev20260615-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-dns:v1-rev20260616-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-drive:v3-rev20260624-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-drive:v3-rev20260712-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-gmail:v1-rev20260525-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-groupssettings:v1-rev20220614-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
com.google.apis:google-api-services-healthcare:v1-rev20240130-2.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -268,19 +269,23 @@ io.grpc:grpc-stub:1.81.0=compileClasspath,deploy_jar,nonprodCompileClasspath,non
io.grpc:grpc-testing:1.70.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-util:1.81.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.grpc:grpc-xds:1.81.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-buffer:4.1.132.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-buffer:4.2.16.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-base:4.2.16.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-compression:4.2.16.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-http2:4.1.132.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-http:4.1.132.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-http:4.2.16.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-marshalling:4.2.16.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-protobuf:4.2.16.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec-socks:4.1.132.Final=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec:4.1.132.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-common:4.1.132.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-codec:4.2.16.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-common:4.2.16.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-handler-proxy:4.1.132.Final=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
io.netty:netty-handler:4.1.132.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-resolver:4.1.132.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-handler:4.2.16.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-resolver:4.2.16.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-tcnative-boringssl-static:2.0.52.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-tcnative-classes:2.0.52.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-transport-native-unix-common:4.1.132.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-transport:4.1.132.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-transport-native-unix-common:4.2.16.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-transport:4.2.16.Final=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opencensus:opencensus-api:0.31.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opencensus:opencensus-contrib-exemplar-util:0.31.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opencensus:opencensus-contrib-grpc-metrics:0.31.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
@@ -441,8 +446,8 @@ org.eclipse.jetty:jetty-server:12.1.11=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-session:12.1.11=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-util:12.1.11=testCompileClasspath,testRuntimeClasspath
org.eclipse.jetty:jetty-xml:12.1.11=testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:12.11.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-database-postgresql:12.11.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:13.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-database-postgresql:13.0.0=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.freemarker:freemarker:2.3.34=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.glassfish.jaxb:codemodel:4.0.9=jaxb
org.glassfish.jaxb:jaxb-core:4.0.6=deploy_jar,nonprodRuntimeClasspath,runtimeClasspath,testRuntimeClasspath
@@ -561,16 +566,16 @@ org.xmlresolver:xmlresolver:5.2.2=checkstyle
org.yaml:snakeyaml:2.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
redis.clients.authentication:redis-authx-core:0.1.1-beta2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
redis.clients:jedis:8.0.0-beta1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson.core:jackson-core:3.1.4=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson.core:jackson-databind:3.1.4=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson:jackson-bom:3.1.4=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson.core:jackson-core:3.1.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson.core:jackson-databind:3.1.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson:jackson-bom:3.1.5=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-api:17.1.7=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-diagram:17.12.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-operations:17.12.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-postgresql:17.12.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-text:17.12.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-diagram:17.12.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-operations:17.12.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-postgresql:17.12.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-text:17.12.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-tools:17.1.7=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-utility:17.1.7=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler:17.12.1=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
us.fatehi:schemacrawler:17.12.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
xerces:xmlParserAPIs:2.6.2=compileClasspath,deploy_jar,nonprodCompileClasspath,nonprodRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
empty=devtool,shadow
@@ -1521,6 +1521,103 @@ public final class RegistryConfig {
.map(valkey -> ImmutableList.copyOf(valkey.hostsAndPorts));
}
@Provides
@Config("eppServerPort")
public static int provideEppServerPort(RegistryConfigSettings config) {
return config.eppServer.port;
}
@Provides
@Config("eppServerHealthCheckPort")
public static int provideEppServerHealthCheckPort(RegistryConfigSettings config) {
return config.eppServer.healthCheckPort;
}
@Provides
@Config("eppServerSslPemBucket")
public static String provideEppServerSslPemBucket(RegistryConfigSettings config) {
return config.eppServer.sslPemBucket;
}
@Provides
@Config("eppServerSslPemFilename")
public static String provideEppServerSslPemFilename(RegistryConfigSettings config) {
return config.eppServer.sslPemFilename;
}
@Provides
@Config("eppServerKmsLocation")
public static String provideEppServerKmsLocation(RegistryConfigSettings config) {
return config.eppServer.kmsLocation;
}
@Provides
@Config("eppServerKmsKeyRing")
public static String provideEppServerKmsKeyRing(RegistryConfigSettings config) {
return config.eppServer.kmsKeyRing;
}
@Provides
@Config("eppServerKmsCryptoKey")
public static String provideEppServerKmsCryptoKey(RegistryConfigSettings config) {
return config.eppServer.kmsCryptoKey;
}
@Provides
@Config("eppServerMaxMessageLengthBytes")
public static int provideEppServerMaxMessageLengthBytes(RegistryConfigSettings config) {
return config.eppServer.maxMessageLengthBytes;
}
@Provides
@Config("eppServerHeaderLengthBytes")
public static int provideEppServerHeaderLengthBytes(RegistryConfigSettings config) {
return config.eppServer.headerLengthBytes;
}
@Provides
@Config("eppServerReadTimeoutSeconds")
public static int provideEppServerReadTimeoutSeconds(RegistryConfigSettings config) {
return config.eppServer.readTimeoutSeconds;
}
@Provides
@Config("eppServerMaxConnectionsPerIp")
public static int provideEppServerMaxConnectionsPerIp(RegistryConfigSettings config) {
return config.eppServer.maxConnectionsPerIp;
}
@Provides
@Config("eppServerMaxConnectionsPerCert")
public static int provideEppServerMaxConnectionsPerCert(RegistryConfigSettings config) {
return config.eppServer.maxConnectionsPerCert;
}
@Provides
@Config("eppServerCertificateCacheSeconds")
public static int provideEppServerCertificateCacheSeconds(RegistryConfigSettings config) {
return config.eppServer.serverCertificateCacheSeconds;
}
@Provides
@Config("eppServerQuota")
public static RegistryConfigSettings.Quota provideEppServerQuota(
RegistryConfigSettings config) {
return config.eppServer.quota;
}
@Provides
@Config("eppServerFrontendMetricsRatio")
public static double provideEppServerFrontendMetricsRatio(RegistryConfigSettings config) {
return config.monitoring.frontendMetricsRatio;
}
@Provides
@Config("writeIntervalSeconds")
public static int provideWriteIntervalSeconds(RegistryConfigSettings config) {
return config.monitoring.writeIntervalSeconds;
}
private static String formatComments(String text) {
return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream()
.map(s -> "# " + s)
@@ -45,6 +45,7 @@ public class RegistryConfigSettings {
public Bsa bsa;
public MosApi mosapi;
public Valkey valkey;
public EppServer eppServer;
/** Configuration options that apply to the entire GCP project. */
public static class GcpProject {
@@ -199,6 +200,42 @@ public class RegistryConfigSettings {
public int stackdriverMaxQps;
public int stackdriverMaxPointsPerRequest;
public int writeIntervalSeconds;
public double frontendMetricsRatio;
public double backendMetricsRatio;
}
/** Configuration for EppServer. */
public static class EppServer {
public int port;
public int healthCheckPort;
public String sslPemFilename;
public String sslPemBucket;
public String kmsLocation;
public String kmsKeyRing;
public String kmsCryptoKey;
public int maxMessageLengthBytes;
public int headerLengthBytes;
public int readTimeoutSeconds;
public int maxConnectionsPerIp;
public int maxConnectionsPerCert;
public int serverCertificateCacheSeconds;
public Quota quota;
}
/** Configuration options that apply to quota management. */
public static class Quota {
/** Quota configuration for a specific set of users. */
public static class QuotaGroup {
public List<String> userId;
public int tokenAmount;
public int refillSeconds;
public int batchSize;
}
public int refreshSeconds;
public QuotaGroup defaultQuota;
public List<QuotaGroup> customQuota;
}
/** Miscellaneous configuration that doesn't quite fit in anywhere else. */
@@ -420,6 +420,57 @@ monitoring:
# How often metrics are exported to BigQuery.
writeIntervalSeconds: 60
# What ratio of frontend request metrics should be stochastically recorded
# (0.0 means none, 1.0 means all). This is useful for reducing metrics volume,
# and thus cost, while still recording some information for performance
# monitoring purposes.
frontendMetricsRatio: 1.0
# What ratio of backend request metrics should be stochastically recorded
# (0.0 means none, 1.0 means all). This is useful for reducing metrics volume,
# and thus cost, while still recording some information for performance
# monitoring purposes.
backendMetricsRatio: 1.0
eppServer:
# The integrated EPP server listens on this port.
port: 30002
# Health check port used by GCP load balancer.
healthCheckPort: 30000
# GCS bucket that stores the encrypted PEM file.
sslPemBucket: your-gcs-bucket
# Name of the encrypted PEM file.
sslPemFilename: your-pem-filename
# Cloud KMS details for certificate decryption.
kmsLocation: your-kms-location
kmsKeyRing: your-kms-keyRing
kmsCryptoKey: your-kms-cryptoKey
# Maximum input message length in bytes.
maxMessageLengthBytes: 1073741824
# Length of the header field in bytes (RFC 5734).
headerLengthBytes: 4
# Time after which an idle connection will be closed.
readTimeoutSeconds: 3600
# Max concurrent connections per IP address.
maxConnectionsPerIp: 10
# Max concurrent connections per authenticated certificate.
maxConnectionsPerCert: 10
# Server certificate cache duration.
serverCertificateCacheSeconds: 1800
# Quota configuration for EPP
quota:
refreshSeconds: 0
defaultQuota:
userId: []
tokenAmount: 100
refillSeconds: 0
# To implement a shared quota group across multiple registrars, place a virtual
# group name as the FIRST element of the userId list.
# e.g., userId: ["my_group", "registrar1", "registrar2"]
customQuota: []
misc:
# The ID of the Google Sheet (as found in the URL) to export registrar details
# to. Leave this null to disable syncing.
@@ -0,0 +1,147 @@
// 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;
import static google.registry.util.ResourceUtils.readResourceBytes;
import com.google.common.collect.ImmutableList;
import dagger.Module;
import dagger.Provides;
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;
import io.netty.channel.ChannelHandler;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.codec.LengthFieldPrepender;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.timeout.ReadTimeoutHandler;
import jakarta.inject.Named;
import jakarta.inject.Provider;
import jakarta.inject.Qualifier;
import jakarta.inject.Singleton;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Optional;
import java.util.function.Supplier;
import redis.clients.jedis.UnifiedJedis;
/** A module that provides the {@link FrontendProtocol} used for epp protocol. */
@Module
public final class EppProtocolModule {
private EppProtocolModule() {}
@Qualifier
public @interface EppProtocol {}
@Qualifier
public @interface IpQuota {}
@Qualifier
public @interface CommandQuota {}
private static final String PROTOCOL_NAME = "epp";
@Singleton
@Provides
@IntoSet
static FrontendProtocol provideProtocol(
@EppProtocol int eppPort,
@EppProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
return Protocol.frontendBuilder()
.name(PROTOCOL_NAME)
.port(eppPort)
.handlerProviders(handlerProviders)
.hasBackend(false)
.build();
}
@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,
lengthFieldPrependerProvider,
eppServiceHandlerProvider);
}
@Provides
static LengthFieldBasedFrameDecoder provideLengthFieldBasedFrameDecoder(
@Config("eppServerMaxMessageLengthBytes") int maxMessageLengthBytes,
@Config("eppServerHeaderLengthBytes") int headerLengthBytes) {
return new LengthFieldBasedFrameDecoder(
maxMessageLengthBytes, 0, headerLengthBytes, -headerLengthBytes, headerLengthBytes);
}
@Singleton
@Provides
static LengthFieldPrepender provideLengthFieldPrepender(
@Config("eppServerHeaderLengthBytes") int headerLengthBytes) {
return new LengthFieldPrepender(headerLengthBytes, true);
}
@Provides
@EppProtocol
static ReadTimeoutHandler provideReadTimeoutHandler(
@Config("eppServerReadTimeoutSeconds") int readTimeoutSeconds) {
return new ReadTimeoutHandler(readTimeoutSeconds);
}
@Singleton
@Provides
@Named("hello")
static byte[] provideHelloBytes() {
try {
return readResourceBytes(EppProtocolModule.class, "hello.xml").read();
} catch (IOException e) {
throw new RuntimeException("Cannot read EPP <hello> message file.", e);
}
}
@Singleton
@Provides
@EppProtocol
static SslServerInitializer<NioSocketChannel> provideSslServerInitializer(
SslProvider sslProvider,
Supplier<PrivateKey> privateKeySupplier,
Supplier<ImmutableList<X509Certificate>> certificatesSupplier) {
return new SslServerInitializer<>(
true, false, sslProvider, privateKeySupplier, certificatesSupplier);
}
@Provides
@Singleton
@CommandQuota
static QuotaManager provideCommandQuotaManager(
@Config("eppServerQuota") RegistryConfigSettings.Quota quota, Optional<UnifiedJedis> jedis) {
return new QuotaManager(quota, jedis.orElse(null), "command");
}
}
@@ -0,0 +1,129 @@
// 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;
import static google.registry.eppserver.Protocol.PROTOCOL_KEY;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.eppserver.EppServerModule.EppServerComponent;
import google.registry.eppserver.Protocol.FrontendProtocol;
import google.registry.eppserver.handler.EppServiceHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.DefaultEventExecutorGroup;
import io.netty.util.concurrent.EventExecutorGroup;
import io.netty.util.concurrent.Future;
import jakarta.inject.Provider;
import java.util.HashMap;
/** An integrated EPP server that listens for EPP traffic and processes it in-process. */
public class EppServer {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** Maximum length of the queue of incoming connections. */
private static final int MAX_SOCKET_BACKLOG = 128;
private final ImmutableSet<FrontendProtocol> protocols;
private final HashMap<Integer, Channel> portToChannelMap = new HashMap<>();
private final EventLoopGroup eventGroup = new NioEventLoopGroup();
private final EventExecutorGroup businessGroup =
new DefaultEventExecutorGroup(Math.max(4, Runtime.getRuntime().availableProcessors() * 4));
public EppServer(EppServerComponent eppServerComponent) {
this.protocols = ImmutableSet.copyOf(eppServerComponent.protocols());
}
private class ServerChannelInitializer extends ChannelInitializer<NioSocketChannel> {
@Override
protected void initChannel(NioSocketChannel inboundChannel) {
FrontendProtocol inboundProtocol =
(FrontendProtocol) inboundChannel.parent().attr(PROTOCOL_KEY).get();
inboundChannel.attr(PROTOCOL_KEY).set(inboundProtocol);
addHandlers(inboundChannel.pipeline(), inboundProtocol.handlerProviders());
// Start reading immediately since we don't have a backend relay.
inboundChannel.config().setAutoRead(true);
logger.atInfo().log("Connection established: %s %s", inboundProtocol.name(), inboundChannel);
}
@SuppressWarnings("deprecation")
private void addHandlers(
ChannelPipeline channelPipeline,
ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
for (Provider<? extends ChannelHandler> handlerProvider : handlerProviders) {
ChannelHandler handler = handlerProvider.get();
String handlerName = handler.getClass().getSimpleName();
if (handler.getClass() == EppServiceHandler.class) {
channelPipeline.addLast(businessGroup, handlerName, handler);
} else {
channelPipeline.addLast(handlerName, handler);
}
}
}
}
public void start() {
ServerBootstrap serverBootstrap =
new ServerBootstrap()
.group(eventGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ServerChannelInitializer())
.option(ChannelOption.SO_BACKLOG, MAX_SOCKET_BACKLOG)
.childOption(ChannelOption.SO_KEEPALIVE, true);
protocols.forEach(
protocol -> {
int port = protocol.port();
try {
ChannelFuture serverChannelFuture = serverBootstrap.bind(port).sync();
if (serverChannelFuture.isSuccess()) {
logger.atInfo().log(
"Start listening on port %s for %s protocol.", port, protocol.name());
Channel serverChannel = serverChannelFuture.channel();
serverChannel.attr(PROTOCOL_KEY).set(protocol);
portToChannelMap.put(port, serverChannel);
}
} catch (InterruptedException e) {
logger.atSevere().withCause(e).log(
"Cannot listen on port %d for %s protocol.", port, protocol.name());
}
});
}
public void stop() {
logger.atInfo().log("Shutting down EPP server...");
portToChannelMap
.values()
.forEach(
channel -> {
Future<?> unusedFuture = channel.close();
});
Future<?> unusedFuture = eventGroup.shutdownGracefully();
Future<?> unusedFutureBusiness = businessGroup.shutdownGracefully();
}
}
@@ -0,0 +1,259 @@
// 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;
import com.google.api.services.cloudkms.v1.CloudKMS;
import com.google.api.services.cloudkms.v1.model.DecryptRequest;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.http.HttpTransportOptions;
import com.google.cloud.storage.BlobId;
import com.google.cloud.storage.Storage;
import com.google.cloud.storage.StorageException;
import com.google.cloud.storage.StorageOptions;
import com.google.common.base.Suppliers;
import com.google.monitoring.metrics.MetricReporter;
import dagger.Component;
import dagger.Module;
import dagger.Provides;
import google.registry.cache.CacheModule;
import google.registry.config.CredentialModule;
import google.registry.config.CredentialModule.ApplicationDefaultCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.eppserver.EppProtocolModule.EppProtocol;
import google.registry.eppserver.HealthCheckProtocolModule.HealthCheckProtocol;
import google.registry.eppserver.Protocol.FrontendProtocol;
import google.registry.keyring.KeyringModule;
import google.registry.keyring.api.KeyModule;
import google.registry.networking.module.CertificateSupplierModule;
import google.registry.networking.module.CertificateSupplierModule.Mode;
import google.registry.privileges.secretmanager.SecretManagerModule;
import google.registry.util.GoogleCredentialsBundle;
import google.registry.util.OidcTokenUtils;
import google.registry.util.RegistryEnvironment;
import google.registry.util.UtilsModule;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslProvider;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import java.io.IOException;
import java.time.Duration;
import java.util.Base64;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import redis.clients.jedis.UnifiedJedis;
/**
* A module that provides the port-to-protocol map and other configs that are used to bootstrap the
* server.
*/
@Module
public class EppServerModule {
@Provides
@EppProtocol
static int provideEppPort(@Config("eppServerPort") int eppPort) {
return eppPort;
}
@Provides
@HealthCheckProtocol
static int provideHealthCheckPort(@Config("eppServerHealthCheckPort") int healthCheckPort) {
return healthCheckPort;
}
@Singleton
@Provides
LoggingHandler provideLoggingHandler() {
return new LoggingHandler(LogLevel.DEBUG);
}
@Singleton
@Provides
static Supplier<GoogleCredentials> provideRefreshedCredentialsSupplier(
@ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle) {
return () -> {
GoogleCredentials credentials = credentialsBundle.getGoogleCredentials();
try {
credentials.refreshIfExpired();
} catch (IOException e) {
throw new RuntimeException("Cannot refresh credentials.", e);
}
return credentials;
};
}
@Singleton
@Provides
@Named("idToken")
static Supplier<String> provideOidcToken(
@ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle,
@Config("oauthClientId") String clientId) {
return Suppliers.memoizeWithExpiration(
() -> OidcTokenUtils.createOidcToken(credentialsBundle, clientId), 1, TimeUnit.HOURS);
}
@Singleton
@Provides
@Named("canary")
static boolean provideIsCanary() {
return RegistryEnvironment.get().name().endsWith("_CANARY");
}
@Singleton
@Provides
static CloudKMS provideCloudKms(
@ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle,
@Config("projectId") String projectId) {
return new CloudKMS.Builder(
credentialsBundle.getHttpTransport(),
credentialsBundle.getJsonFactory(),
credentialsBundle.getHttpRequestInitializer())
.setApplicationName(projectId)
.build();
}
@Singleton
@Provides
static Storage provideStorage(
@ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle) {
return StorageOptions.newBuilder()
.setTransportOptions(
HttpTransportOptions.newBuilder()
.setHttpTransportFactory(credentialsBundle::getHttpTransport)
.build())
.setCredentials(credentialsBundle.getGoogleCredentials())
.build()
.getService();
}
@Provides
@Named("encryptedPemBytes")
static byte[] provideEncryptedPemBytes(
Storage storage,
@Config("eppServerSslPemBucket") String bucket,
@Config("eppServerSslPemFilename") String sslPemFilename) {
try {
return Base64.getMimeDecoder()
.decode(storage.readAllBytes(BlobId.of(bucket, sslPemFilename)));
} catch (StorageException e) {
throw new RuntimeException(
String.format(
"Error reading encrypted PEM file %s from GCS bucket %s", sslPemFilename, bucket),
e);
}
}
@Provides
@Named("pemBytes")
static byte[] providePemBytes(
CloudKMS cloudKms,
@Named("encryptedPemBytes") byte[] encryptedPemBytes,
@Config("projectId") String projectId,
@Config("eppServerKmsLocation") String location,
@Config("eppServerKmsKeyRing") String keyRing,
@Config("eppServerKmsCryptoKey") String cryptoKey) {
String cryptoKeyUrl =
String.format(
"projects/%s/locations/%s/keyRings/%s/cryptoKeys/%s",
projectId, location, keyRing, cryptoKey);
try {
DecryptRequest decryptRequest = new DecryptRequest().encodeCiphertext(encryptedPemBytes);
return cloudKms
.projects()
.locations()
.keyRings()
.cryptoKeys()
.decrypt(cryptoKeyUrl, decryptRequest)
.execute()
.decodePlaintext();
} catch (IOException e) {
throw new RuntimeException(
String.format("PEM file decryption failed using CryptoKey: %s", cryptoKeyUrl), e);
}
}
@Provides
static SslProvider provideSslProvider() {
return OpenSsl.isAvailable() ? SslProvider.OPENSSL : SslProvider.JDK;
}
@Provides
static ExecutorService provideExecutorService() {
return Executors.newWorkStealingPool();
}
@Provides
static ScheduledExecutorService provideScheduledExecutorService() {
return Executors.newSingleThreadScheduledExecutor();
}
@Singleton
@Provides
static Mode provideMode() {
if (RegistryEnvironment.get() == RegistryEnvironment.LOCAL) {
return Mode.SELF_SIGNED;
} else {
return Mode.PEM_FILE;
}
}
@Singleton
@Provides
@Named("remoteCertCachingDuration")
static Duration provideCertCachingDuration(
@Config("eppServerCertificateCacheSeconds") int cacheSeconds) {
return Duration.ofSeconds(cacheSeconds);
}
@Singleton
@Provides
@Named("frontendMetricsRatio")
static double provideFrontendMetricsRatio(@Config("eppServerFrontendMetricsRatio") double ratio) {
return ratio;
}
/** Root level component that exposes the port-to-protocol map. */
@Singleton
@Component(
modules = {
EppServerModule.class,
CacheModule.class,
CertificateSupplierModule.class,
ConfigModule.class,
EppProtocolModule.class,
HealthCheckProtocolModule.class,
MetricsModule.class,
CredentialModule.class,
KeyModule.class,
KeyringModule.class,
SecretManagerModule.class,
UtilsModule.class
})
public interface EppServerComponent {
Set<FrontendProtocol> protocols();
MetricReporter metricReporter();
Optional<UnifiedJedis> jedis();
}
}
@@ -0,0 +1,66 @@
// 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;
import com.google.common.collect.ImmutableList;
import dagger.Module;
import dagger.Provides;
import dagger.multibindings.IntoSet;
import google.registry.eppserver.Protocol.FrontendProtocol;
import google.registry.eppserver.handler.HealthCheckHandler;
import io.netty.channel.ChannelHandler;
import jakarta.inject.Provider;
import jakarta.inject.Qualifier;
import jakarta.inject.Singleton;
/** A module that provides the {@link FrontendProtocol} used for health check protocol. */
@Module
public final class HealthCheckProtocolModule {
/** Dagger qualifier to provide health check protocol related handlers and other bindings. */
@Qualifier
public @interface HealthCheckProtocol {}
private static final String PROTOCOL_NAME = "health_check";
@Singleton
@Provides
@IntoSet
static FrontendProtocol provideProtocol(
@HealthCheckProtocol int healthCheckPort,
@HealthCheckProtocol ImmutableList<Provider<? extends ChannelHandler>> handlerProviders) {
return Protocol.frontendBuilder()
.name(PROTOCOL_NAME)
.port(healthCheckPort)
.handlerProviders(handlerProviders)
.hasBackend(false)
.build();
}
@Provides
@HealthCheckProtocol
static ImmutableList<Provider<? extends ChannelHandler>> provideHandlerProviders(
@HealthCheckProtocol Provider<HealthCheckHandler> healthCheckHandlerProvider) {
return ImmutableList.of(healthCheckHandlerProvider);
}
@Provides
@HealthCheckProtocol
static HealthCheckHandler provideHealthCheckHandler() {
// These are currently hardcoded in the handler or defaulted in RegistryConfig.
// For now, we use the standard GCP health check strings.
return new HealthCheckHandler("HEALTH_CHECK_REQUEST", "HEALTH_CHECK_RESPONSE");
}
}
@@ -0,0 +1,115 @@
// 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;
import com.google.api.services.monitoring.v3.Monitoring;
import com.google.api.services.monitoring.v3.model.MonitoredResource;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.monitoring.metrics.MetricReporter;
import com.google.monitoring.metrics.MetricWriter;
import com.google.monitoring.metrics.stackdriver.StackdriverWriter;
import dagger.Component;
import dagger.Module;
import dagger.Provides;
import google.registry.config.CredentialModule;
import google.registry.config.CredentialModule.ApplicationDefaultCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.util.GoogleCredentialsBundle;
import google.registry.util.MetricParameters;
import google.registry.util.RegistryEnvironment;
import jakarta.inject.Singleton;
/** Module that provides necessary bindings to instantiate a {@link MetricReporter} */
@Module
public class MetricsModule {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Singleton
@Provides
static Monitoring provideMonitoring(
@ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle,
@Config("projectId") String projectId) {
return new Monitoring.Builder(
credentialsBundle.getHttpTransport(),
credentialsBundle.getJsonFactory(),
credentialsBundle.getHttpRequestInitializer())
.setApplicationName(projectId)
.build();
}
@Singleton
@Provides
static MetricWriter provideMetricWriter(
Monitoring monitoringClient,
MonitoredResource monitoredResource,
@Config("projectId") String projectId,
@Config("stackdriverMaxQps") int stackdriverMaxQps,
@Config("stackdriverMaxPointsPerRequest") int stackdriverMaxPointsPerRequest) {
return new StackdriverWriter(
monitoringClient,
projectId,
monitoredResource,
stackdriverMaxQps,
stackdriverMaxPointsPerRequest);
}
@Singleton
@Provides
static MetricReporter provideMetricReporter(
MetricWriter metricWriter, @Config("writeIntervalSeconds") int writeIntervalSeconds) {
return new MetricReporter(
metricWriter, writeIntervalSeconds, new ThreadFactoryBuilder().setDaemon(true).build());
}
/**
* Provides a {@link MonitoredResource} appropriate for environment the EPP server runs in.
*
* <p>When running locally, the type of the monitored resource is set to {@code global}, otherwise
* it is {@code gke_container}.
*
* @see <a
* href="https://cloud.google.com/monitoring/custom-metrics/creating-metrics#which-resource">
* Choosing a monitored resource type</a>
*/
@Singleton
@Provides
static MonitoredResource provideMonitoredResource(
@Config("projectId") String projectId, MetricParameters metricParameters) {
MonitoredResource monitoredResource = new MonitoredResource();
if (RegistryEnvironment.get() == RegistryEnvironment.LOCAL) {
monitoredResource.setType("global").setLabels(ImmutableMap.of("project_id", projectId));
} else {
monitoredResource.setType("gke_container").setLabels(metricParameters.makeLabelsMap());
}
logger.atInfo().log("Monitored resource: %s", monitoredResource);
return monitoredResource;
}
@Singleton
@Component(
modules = {
MetricsModule.class,
EppServerModule.class,
CredentialModule.class,
ConfigModule.class
})
interface MetricsComponent {
MetricReporter metricReporter();
}
}
@@ -0,0 +1,147 @@
// 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;
import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.Attribute;
import io.netty.util.AttributeKey;
import jakarta.inject.Provider;
import javax.annotation.Nullable;
/** Value class that encapsulates parameters of a specific connection. */
public interface Protocol {
/** Key used to retrieve the {@link Protocol} from a {@link Channel}'s {@link Attribute}. */
AttributeKey<Protocol> PROTOCOL_KEY = AttributeKey.valueOf("PROTOCOL_KEY");
/** Protocol name. */
String name();
/**
* Port to bind to (for {@link FrontendProtocol}) or to connect to (for {@link BackendProtocol}).
*/
int port();
/** The {@link ChannelHandler} providers to use for the protocol, in order. */
ImmutableList<Provider<? extends ChannelHandler>> handlerProviders();
/** A builder for {@link FrontendProtocol}, by default there is a backend associated with it. */
static FrontendProtocol.Builder frontendBuilder() {
return new AutoValue_Protocol_FrontendProtocol.Builder().hasBackend(true);
}
/** A builder for {@link FrontendProtocol}, by default it connects to a remote host. */
static BackendProtocol.Builder backendBuilder() {
return new AutoValue_Protocol_BackendProtocol.Builder().isLocal(false);
}
/**
* Generic builder enabling chaining for concrete implementations.
*
* @param <B> builder of the concrete subtype of {@link Protocol}.
* @param <P> type of the concrete subtype of {@link Protocol}.
*/
abstract class Builder<B extends Builder<B, P>, P extends Protocol> {
public abstract B name(String value);
public abstract B port(int port);
public abstract B handlerProviders(ImmutableList<Provider<? extends ChannelHandler>> value);
public abstract P build();
}
/**
* Connection parameters for a connection from the client to the proxy.
*
* <p>This protocol is associated to a {@link NioSocketChannel} established by remote peer
* connecting to the given {@code port} that the proxy is listening on.
*/
@AutoValue
abstract class FrontendProtocol implements Protocol {
/**
* The {@link BackendProtocol} used to establish a relay channel and relay the traffic to. Not
* required for health check protocol or HTTP(S) redirect.
*/
@Nullable
public abstract BackendProtocol relayProtocol();
/**
* Whether this {@code FrontendProtocol} relays to a {@code BackendProtocol}. All proxied
* traffic must be represented by a protocol that has a backend.
*/
public abstract boolean hasBackend();
@AutoValue.Builder
public abstract static class Builder extends Protocol.Builder<Builder, FrontendProtocol> {
public abstract Builder relayProtocol(BackendProtocol value);
public abstract Builder hasBackend(boolean value);
abstract FrontendProtocol autoBuild();
@Override
public FrontendProtocol build() {
FrontendProtocol frontendProtocol = autoBuild();
Preconditions.checkState(
!frontendProtocol.hasBackend() || frontendProtocol.relayProtocol() != null,
"Frontend protocol %s must define a relay protocol.",
frontendProtocol.name());
return frontendProtocol;
}
}
}
/**
* Connection parameters for a connection from the EPP server to Nomulus.
*
* <p>This protocol is associated to a {@link NioSocketChannel} established by the EPP server
* connecting to a remote peer.
*/
@AutoValue
abstract class BackendProtocol implements Protocol {
/** The hostname that the EPP server connects to. */
public abstract String host();
/** Whether the protocol is expected to connect to localhost. */
public abstract boolean isLocal();
/** Builder of {@link BackendProtocol}. */
@AutoValue.Builder
public abstract static class Builder extends Protocol.Builder<Builder, BackendProtocol> {
public abstract Builder host(String value);
public abstract Builder isLocal(boolean value);
abstract BackendProtocol autoBuild();
@Override
public BackendProtocol build() {
BackendProtocol protocol = autoBuild();
Preconditions.checkState(
!protocol.isLocal() || protocol.host().equals("localhost"),
"Local backend protocol must connect to localhost");
return autoBuild();
}
}
}
}
@@ -0,0 +1,199 @@
// 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 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) {
remoteIP = headerArray[2];
logger.atFine().log(
"Header parsed, using %s as remote IP for channel %s", remoteIP, ctx.channel());
// 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;
}
}
@@ -0,0 +1,233 @@
// 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.
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;
import static google.registry.util.GcpJsonFormatter.unsetCurrentRequest;
import static google.registry.util.X509Utils.getCertificateHash;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.eppserver.EppProtocolModule.CommandQuota;
import google.registry.eppserver.metric.FrontendMetrics;
import google.registry.eppserver.quota.LocalConnectionLimiter;
import google.registry.eppserver.quota.QuotaManager;
import google.registry.module.RegistryServlet;
import google.registry.request.RequestHandler;
import google.registry.util.FakeHttpServletRequest;
import google.registry.util.FakeHttpServletResponse;
import google.registry.util.ProxyHttpHeaders;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.AttributeKey;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.Promise;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.security.cert.X509Certificate;
import java.util.UUID;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Unified processor for EPP protocol traffic.
*
* <p>Consolidates throttling, session management, and in-process execution. Extracts registrar ID
* (clID) directly from EPP login XML for accurate throttling.
*/
public class EppServiceHandler extends SimpleChannelInboundHandler<ByteBuf> {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
// Fast regex to extract clID from EPP login command without full XML parsing.
private static final Pattern CLID_PATTERN =
Pattern.compile("<clID>([^<]+)</clID>", Pattern.CASE_INSENSITIVE);
public static final AttributeKey<String> CLIENT_CERTIFICATE_HASH_KEY =
AttributeKey.valueOf("CLIENT_CERTIFICATE_HASH_KEY");
private final byte[] helloBytes;
private final FrontendMetrics metrics;
private final LocalConnectionLimiter localConnectionLimiter;
private final QuotaManager commandQuotaManager;
private final Supplier<String> idTokenSupplier;
private final String projectId;
private String sslClientCertificateHash;
private String clientAddress;
private String registrarId; // The clID extracted from login
private String sessionCookie;
private boolean ipAcquired = false;
private boolean certAcquired = false;
@VisibleForTesting RequestHandler<?> requestHandler = RegistryServlet.component.requestHandler();
@Inject
public EppServiceHandler(
@Named("hello") byte[] helloBytes,
FrontendMetrics metrics,
LocalConnectionLimiter localConnectionLimiter,
@CommandQuota QuotaManager commandQuotaManager,
@Named("idToken") Supplier<String> idTokenSupplier,
@Config("projectId") String projectId) {
this.helloBytes = helloBytes.clone();
this.metrics = metrics;
this.localConnectionLimiter = localConnectionLimiter;
this.commandQuotaManager = commandQuotaManager;
this.idTokenSupplier = idTokenSupplier;
this.projectId = projectId;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
Promise<X509Certificate> certPromise = ctx.channel().attr(CLIENT_CERTIFICATE_PROMISE_KEY).get();
if (certPromise != null) {
certPromise.addListener(
(Promise<X509Certificate> promise) -> {
if (promise.isSuccess()) {
ctx.executor().execute(() -> onSslHandshakeComplete(ctx, promise.getNow()));
} else {
logger.atWarning().withCause(promise.cause()).log("SSL handshake failed");
@SuppressWarnings("unused")
Future<?> unusedFuture = ctx.close();
}
});
}
super.channelActive(ctx);
}
private void onSslHandshakeComplete(ChannelHandlerContext ctx, X509Certificate cert) {
sslClientCertificateHash = getCertificateHash(cert);
clientAddress = ctx.channel().attr(REMOTE_ADDRESS_KEY).get();
ctx.channel().attr(CLIENT_CERTIFICATE_HASH_KEY).set(sslClientCertificateHash);
// 1. Connection throttling (IP and Certificate)
if (!localConnectionLimiter.acquireIp(clientAddress)) {
metrics.registerQuotaRejection("epp_connection_ip", clientAddress);
@SuppressWarnings("unused")
Future<?> unusedFuture = ctx.close();
return;
}
ipAcquired = true;
if (!localConnectionLimiter.acquireCert(sslClientCertificateHash)) {
metrics.registerQuotaRejection("epp_connection", sslClientCertificateHash);
@SuppressWarnings("unused")
Future<?> unusedFuture = ctx.close();
return;
}
certAcquired = true;
metrics.registerActiveConnection("epp", sslClientCertificateHash, ctx.channel());
// 2. Trigger initial EPP <greeting>
handleEppFrame(ctx, Unpooled.wrappedBuffer(helloBytes));
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf frame) {
handleEppFrame(ctx, frame);
}
private void handleEppFrame(ChannelHandlerContext ctx, ByteBuf frame) {
String xml = frame.toString(UTF_8);
// 1. Maturing Identity: If we don't have clID yet, try to extract it from a login command.
if (registrarId == null) {
Matcher matcher = CLID_PATTERN.matcher(xml);
if (matcher.find()) {
registrarId = matcher.group(1).trim();
logger.atInfo().log("Identified registrar: %s", registrarId);
}
}
// 2. Command-level rate limiting
// Use clID if identified, otherwise fallback to cert hash (for the login command itself).
String throttleId = (registrarId != null) ? registrarId : sslClientCertificateHash;
if (throttleId != null) {
if (!commandQuotaManager.acquireQuota(new QuotaManager.QuotaRequest(throttleId)).success()) {
metrics.registerQuotaRejection("epp_command", throttleId);
@SuppressWarnings("unused")
Future<?> unusedFuture = ctx.close();
return;
}
}
// 3. Execute command in-process
FakeHttpServletRequest req = new FakeHttpServletRequest();
req.setRequestUri("/_dr/epp");
req.setBody(xml.getBytes(UTF_8));
req.setHeader(ProxyHttpHeaders.CERTIFICATE_HASH, sslClientCertificateHash);
req.setHeader(ProxyHttpHeaders.IP_ADDRESS, clientAddress);
if (registrarId != null) {
req.setHeader(ProxyHttpHeaders.REGISTRAR_ID, registrarId);
}
if (sessionCookie != null) {
req.setHeader("Cookie", sessionCookie);
}
req.setHeader("Authorization", "Bearer " + idTokenSupplier.get());
FakeHttpServletResponse rsp = new FakeHttpServletResponse();
String traceId =
String.format(
"projects/%s/traces/%s", projectId, UUID.randomUUID().toString().replace("-", ""));
setCurrentTraceId(traceId);
setCurrentRequest("POST", "/_dr/epp", "Netty-EPP", "EPP/1.0");
try {
requestHandler.handleRequest(req, rsp);
String setCookie = rsp.getHeader("Set-Cookie");
if (setCookie != null) {
sessionCookie = setCookie;
}
ByteBuf out = Unpooled.wrappedBuffer(rsp.getPayload());
if ("close".equals(rsp.getHeader(ProxyHttpHeaders.EPP_SESSION))) {
@SuppressWarnings("unused")
Future<?> unusedFuture = ctx.writeAndFlush(out).addListener(ChannelFutureListener.CLOSE);
} else {
@SuppressWarnings("unused")
Future<?> unusedFuture = ctx.writeAndFlush(out);
}
} catch (Exception e) {
logger.atSevere().withCause(e).log("Internal EPP processing error");
@SuppressWarnings("unused")
Future<?> unusedFuture = ctx.close();
} finally {
setCurrentTraceId(null);
unsetCurrentRequest();
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (certAcquired) {
localConnectionLimiter.releaseCert(sslClientCertificateHash);
}
if (ipAcquired) {
localConnectionLimiter.releaseIp(clientAddress);
}
super.channelInactive(ctx);
}
}
@@ -0,0 +1,43 @@
// 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 io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.nio.charset.StandardCharsets;
/** A handler that responds to GCP load balancer health check message */
public class HealthCheckHandler extends ChannelInboundHandlerAdapter {
private final ByteBuf checkRequest;
private final ByteBuf checkResponse;
public HealthCheckHandler(String checkRequest, String checkResponse) {
this.checkRequest = Unpooled.wrappedBuffer(checkRequest.getBytes(StandardCharsets.US_ASCII));
this.checkResponse = Unpooled.wrappedBuffer(checkResponse.getBytes(StandardCharsets.US_ASCII));
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf) msg;
if (buf.equals(checkRequest)) {
ChannelFuture unusedFuture = ctx.writeAndFlush(checkResponse);
}
buf.release();
}
}
@@ -0,0 +1,61 @@
// Copyright 2019 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.metric;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.monitoring.metrics.CustomFitter;
import com.google.monitoring.metrics.ExponentialFitter;
import com.google.monitoring.metrics.FibonacciFitter;
import com.google.monitoring.metrics.LabelDescriptor;
/** Base class for metrics. */
public abstract class BaseMetrics {
/**
* Labels to register metrics with.
*
* <p>The client certificate hash value is only used for EPP metrics. In order to get the actual
* registrar name, one can use the {@code nomulus} tool:
*
* <pre>
* nomulus -e production list_registrars -f clientCertificateHash | grep $HASH
* </pre>
*/
protected static final ImmutableSet<LabelDescriptor> LABELS =
ImmutableSet.of(
LabelDescriptor.create("protocol", "Name of the protocol."),
LabelDescriptor.create(
"client_cert_hash", "SHA256 hash of the client certificate, if available."));
// Maximum request size is defined in the config file, this is not realistic and we'd be out of
// memory when the size approaches 1 GB.
protected static final CustomFitter DEFAULT_SIZE_FITTER = FibonacciFitter.create(1073741824);
// Maximum 1 hour latency, this is not specified by the spec, but given we have a one hour idle
// timeout, it seems reasonable that maximum latency is set to 1 hour as well. If we are
// approaching anywhere near 1 hour latency, we'd be way out of SLO anyway.
protected static final ExponentialFitter DEFAULT_LATENCY_FITTER =
ExponentialFitter.create(22, 2, 1.0);
/**
* Resets all metrics.
*
* <p>This should only be used in tests to reset states. Production code should not call this
* method.
*/
@VisibleForTesting
abstract void resetMetrics();
}
@@ -0,0 +1,126 @@
// 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.metric;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.monitoring.metrics.EventMetric;
import com.google.monitoring.metrics.IncrementableMetric;
import com.google.monitoring.metrics.Metric;
import com.google.monitoring.metrics.MetricRegistryImpl;
import google.registry.util.NonFinalForTesting;
import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.inject.Singleton;
import java.time.Duration;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/** Frontend metrics instrumentation. */
@Singleton
public class FrontendMetrics extends BaseMetrics {
private static final ConcurrentMap<ImmutableList<String>, ChannelGroup> activeConnections =
new ConcurrentHashMap<>();
static final Metric<Long> activeConnectionsGauge =
MetricRegistryImpl.getDefault()
.newGauge(
"/eppserver/active_connections",
"Number of active connections from clients to the EPP server.",
"Active Connections",
LABELS,
() ->
activeConnections.entrySet().stream()
.collect(
ImmutableMap.toImmutableMap(
Map.Entry::getKey, entry -> (long) entry.getValue().size())),
Long.class);
static final IncrementableMetric totalConnectionsCounter =
MetricRegistryImpl.getDefault()
.newIncrementableMetric(
"/eppserver/total_connections",
"Total number connections ever made from clients to the EPP server.",
"Total Connections",
LABELS);
static final IncrementableMetric quotaRejectionsCounter =
MetricRegistryImpl.getDefault()
.newIncrementableMetric(
"/eppserver/quota_rejections",
"Total number rejected quota request made by EPP server for each connection.",
"Quota Rejections",
LABELS);
static final EventMetric latencyMs =
MetricRegistryImpl.getDefault()
.newEventMetric(
"/eppserver/latency_ms",
"Round-trip time between a request received and its corresponding response is sent.",
"Latency Milliseconds",
LABELS,
DEFAULT_LATENCY_FITTER);
private final Random random;
private final double frontendMetricsRatio;
@Inject
FrontendMetrics(@Named("frontendMetricsRatio") double frontendMetricsRatio, Random random) {
this.frontendMetricsRatio = frontendMetricsRatio;
this.random = random;
}
@Override
void resetMetrics() {
totalConnectionsCounter.reset();
activeConnections.clear();
latencyMs.reset();
}
@NonFinalForTesting
public void registerActiveConnection(String protocol, String certHash, Channel channel) {
totalConnectionsCounter.increment(protocol, certHash);
ImmutableList<String> labels = ImmutableList.of(protocol, certHash);
ChannelGroup channelGroup;
if (activeConnections.containsKey(labels)) {
channelGroup = activeConnections.get(labels);
} else {
channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
activeConnections.put(labels, channelGroup);
}
channelGroup.add(channel);
}
@NonFinalForTesting
public void registerQuotaRejection(String protocol, String certHash) {
quotaRejectionsCounter.increment(protocol, certHash);
}
@NonFinalForTesting
public void responseSent(String protocol, String certHash, Duration latency) {
// Short-circuit metrics recording randomly according to the configured ratio.
if (random.nextDouble() > frontendMetricsRatio) {
return;
}
latencyMs.record(latency.toMillis(), protocol, certHash);
}
}
@@ -0,0 +1,94 @@
// 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.
package google.registry.eppserver.quota;
import google.registry.config.RegistryConfig.Config;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import java.util.concurrent.ConcurrentHashMap;
import javax.annotation.concurrent.ThreadSafe;
/**
* Thread-safe, in-memory rate limiter for restricting the number of concurrent connections allowed
* per IP address and per authenticated certificate.
*/
@ThreadSafe
@Singleton
public class LocalConnectionLimiter {
private final int maxConnectionsPerIp;
private final int maxConnectionsPerCert;
private final ConcurrentHashMap<String, Integer> ipConnections = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Integer> certConnections = new ConcurrentHashMap<>();
@Inject
public LocalConnectionLimiter(
@Config("eppServerMaxConnectionsPerIp") int maxConnectionsPerIp,
@Config("eppServerMaxConnectionsPerCert") int maxConnectionsPerCert) {
this.maxConnectionsPerIp = maxConnectionsPerIp;
this.maxConnectionsPerCert = maxConnectionsPerCert;
}
/** Attempts to acquire a slot for the given IP address. */
public boolean acquireIp(String ipAddress) {
return acquire(ipAddress, ipConnections, maxConnectionsPerIp);
}
/** Releases a slot for the given IP address. */
public void releaseIp(String ipAddress) {
release(ipAddress, ipConnections);
}
/** Attempts to acquire a slot for the given certificate hash. */
public boolean acquireCert(String certHash) {
return acquire(certHash, certConnections, maxConnectionsPerCert);
}
/** Releases a slot for the given certificate hash. */
public void releaseCert(String certHash) {
release(certHash, certConnections);
}
private boolean acquire(String key, ConcurrentHashMap<String, Integer> map, int limit) {
boolean[] acquired = new boolean[1];
map.compute(
key,
(k, v) -> {
if (v == null) {
acquired[0] = true;
return 1;
}
if (v < limit) {
acquired[0] = true;
return v + 1;
}
acquired[0] = false;
return v;
});
return acquired[0];
}
private void release(String key, ConcurrentHashMap<String, Integer> map) {
map.computeIfPresent(
key,
(k, v) -> {
if (v <= 1) {
return null;
}
return v - 1;
});
}
}
@@ -0,0 +1,171 @@
// 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.quota;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfigSettings.Quota;
import google.registry.config.RegistryConfigSettings.Quota.QuotaGroup;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import redis.clients.jedis.UnifiedJedis;
/**
* Unified manager for distributed quota enforcement using Redis/Valkey.
*
* <p>Handles both configuration lookup and atomic Redis operations for connection and command-level
* throttling.
*/
@ThreadSafe
public class QuotaManager {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final int DEFAULT_TTL_SECONDS = 3600;
/** Lua script to atomically decrement a token bucket with a TTL. */
private static final String DECR_LUA =
"local current = redis.call('GET', KEYS[1]) "
+ "if not current then "
+ " redis.call('SET', KEYS[1], ARGV[1] - 1, 'EX', ARGV[2]) "
+ " return tonumber(ARGV[1]) - 1 "
+ "end "
+ "if tonumber(current) <= 0 then "
+ " return -1 "
+ "end "
+ "return redis.call('DECR', KEYS[1])";
/** Lua script to atomically increment back a connection token (capped at max). */
private static final String INCR_LUA =
"local current = redis.call('GET', KEYS[1]) "
+ "if current and tonumber(current) < tonumber(ARGV[1]) then "
+ " return redis.call('INCR', KEYS[1]) "
+ "end "
+ "return nil";
/** Lua script to refresh the TTL of an existing token bucket. */
private static final String EXPIRE_LUA =
"if redis.call('EXISTS', KEYS[1]) == 1 then "
+ " return redis.call('EXPIRE', KEYS[1], ARGV[1]) "
+ "end "
+ "return 0";
private final UnifiedJedis jedis;
private final String quotaNamespace;
private final QuotaGroup defaultQuota;
private final ImmutableMap<String, QuotaGroup> customQuotas;
public QuotaManager(Quota quota, @Nullable UnifiedJedis jedis, String quotaNamespace) {
this.jedis = jedis;
this.quotaNamespace = quotaNamespace;
this.defaultQuota = quota.defaultQuota;
ImmutableMap.Builder<String, QuotaGroup> builder = ImmutableMap.builder();
quota.customQuota.forEach(group -> group.userId.forEach(userId -> builder.put(userId, group)));
this.customQuotas = builder.build();
}
public record QuotaRequest(String userId) {}
public record QuotaResponse(boolean success) {}
public record QuotaRebate(String userId) {}
/** Attempts to acquire a quota token from Redis. */
public QuotaResponse acquireQuota(QuotaRequest request) {
String userId = request.userId();
QuotaGroup group = customQuotas.getOrDefault(userId, defaultQuota);
// Unlimited quota check
if (group.tokenAmount < 0) {
return new QuotaResponse(true);
}
if (jedis == null) {
return new QuotaResponse(true); // Fail open if no Valkey configured
}
// Use the first ID as the virtual group identity if it's a custom group,
// otherwise isolate each default user by their actual ID.
String redisId =
(group == defaultQuota || group.userId.isEmpty()) ? userId : group.userId.get(0);
String key = String.format("%s:%s", quotaNamespace, redisId);
int ttl = group.refillSeconds > 0 ? group.refillSeconds : DEFAULT_TTL_SECONDS;
try {
Object result =
jedis.eval(DECR_LUA, 1, key, String.valueOf(group.tokenAmount), String.valueOf(ttl));
return new QuotaResponse(((Long) result) >= 0);
} catch (Exception e) {
logger.atSevere().withCause(e).log(
"Valkey error for quota key: %s", URLEncoder.encode(key, StandardCharsets.UTF_8));
return new QuotaResponse(true); // Fail open
}
}
/** Refreshes the TTL of an existing quota token. */
public void refreshQuota(QuotaRequest request) {
if (jedis == null) {
return;
}
String userId = request.userId();
QuotaGroup group = customQuotas.getOrDefault(userId, defaultQuota);
if (group.tokenAmount < 0) {
return;
}
// Use the first ID as the virtual group identity if it's a custom group,
// otherwise isolate each default user by their actual ID.
String redisId =
(group == defaultQuota || group.userId.isEmpty()) ? userId : group.userId.get(0);
String key = String.format("%s:%s", quotaNamespace, redisId);
int ttl = group.refillSeconds > 0 ? group.refillSeconds : DEFAULT_TTL_SECONDS;
try {
jedis.eval(EXPIRE_LUA, 1, key, String.valueOf(ttl));
} catch (Exception e) {
logger.atSevere().withCause(e).log(
"Valkey error refreshing quota for: %s", URLEncoder.encode(key, StandardCharsets.UTF_8));
}
}
/** Returns a token to the pool (used for connection throttling). */
public void releaseQuota(QuotaRebate rebate) {
if (jedis == null) {
return;
}
String userId = rebate.userId();
QuotaGroup group = customQuotas.getOrDefault(userId, defaultQuota);
if (group.tokenAmount < 0) {
return;
}
// Use the first ID as the virtual group identity if it's a custom group,
// otherwise isolate each default user by their actual ID.
String redisId =
(group == defaultQuota || group.userId.isEmpty()) ? userId : group.userId.get(0);
String key = String.format("%s:%s", quotaNamespace, redisId);
try {
jedis.eval(INCR_LUA, 1, key, String.valueOf(group.tokenAmount));
} catch (Exception e) {
logger.atSevere().withCause(e).log(
"Valkey error releasing quota for: %s", URLEncoder.encode(key, StandardCharsets.UTF_8));
}
}
}
@@ -260,7 +260,7 @@ public class TlsCredentials implements TransportCredentials {
static Optional<InetAddress> provideIpAddress(HttpServletRequest req) {
Optional<String> clientAddress = extractOptionalHeader(req, ProxyHttpHeaders.IP_ADDRESS);
Optional<String> fallbackClientAddress =
extractOptionalHeader(req, ProxyHttpHeaders.IP_ADDRESS);
extractOptionalHeader(req, ProxyHttpHeaders.FALLBACK_IP_ADDRESS);
Optional<InetAddress> clientInetAddr = clientAddress.map(TlsCredentials::parseInetAddress);
return clientInetAddr.isPresent()
? clientInetAddr
@@ -0,0 +1,78 @@
// 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.module;
import com.google.common.flogger.FluentLogger;
import google.registry.eppserver.DaggerEppServerModule_EppServerComponent;
import google.registry.eppserver.EppServer;
import google.registry.eppserver.EppServerModule;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
/** A {@link ServletContextListener} that starts and stops the integrated Netty EPP server. */
@WebListener
public class EppServerLifecycleListener implements ServletContextListener {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private EppServer eppServer;
private EppServerModule.EppServerComponent eppServerComponent;
@Override
public void contextInitialized(ServletContextEvent sce) {
if (!Boolean.parseBoolean(System.getenv("TCP_SERVER_ENABLED"))) {
logger.atInfo().log(
"TCP_SERVER_ENABLED is false or not set. Skipping integrated EPP server initialization.");
return;
}
logger.atInfo().log("Initializing integrated EPP server...");
try {
EppServerModule eppServerModule = new EppServerModule();
eppServerComponent =
DaggerEppServerModule_EppServerComponent.builder()
.eppServerModule(eppServerModule)
.build();
eppServer = new EppServer(eppServerComponent);
eppServer.start();
logger.atInfo().log("Integrated EPP server started successfully.");
} catch (Exception e) {
logger.atSevere().withCause(e).log("Failed to start integrated EPP server.");
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
if (eppServer != null) {
logger.atInfo().log("Stopping integrated EPP server...");
eppServer.stop();
logger.atInfo().log("Integrated EPP server stopped.");
}
if (eppServerComponent != null) {
eppServerComponent
.jedis()
.ifPresent(
jedis -> {
try {
jedis.close();
logger.atInfo().log("Closed UnifiedJedis client.");
} catch (Exception e) {
logger.atSevere().withCause(e).log("Failed to close UnifiedJedis client.");
}
});
}
}
}
@@ -0,0 +1,54 @@
// 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.
package google.registry.module;
import com.google.common.flogger.FluentLogger;
import com.google.monitoring.metrics.MetricReporter;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import jakarta.servlet.annotation.WebListener;
import java.time.Duration;
/** Starts and stops the single global JVM MetricReporter on application startup/shutdown. */
@WebListener
public class MetricsLifecycleListener implements ServletContextListener {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private MetricReporter metricReporter;
@Override
public void contextInitialized(ServletContextEvent sce) {
logger.atInfo().log("Initializing global MetricReporter...");
try {
metricReporter = RegistryServlet.component.metricReporter().get();
metricReporter.startAsync().awaitRunning(Duration.ofSeconds(10));
logger.atInfo().log("Global MetricReporter started successfully.");
} catch (Exception e) {
logger.atSevere().withCause(e).log("Failed to start global MetricReporter.");
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
if (metricReporter != null) {
try {
metricReporter.stopAsync().awaitTerminated(Duration.ofSeconds(10));
logger.atInfo().log("Global MetricReporter stopped.");
} catch (Exception e) {
logger.atSevere().withCause(e).log("Failed to stop global MetricReporter.");
}
}
}
}
@@ -38,8 +38,6 @@ import google.registry.groups.GroupsModule;
import google.registry.groups.GroupssettingsModule;
import google.registry.keyring.KeyringModule;
import google.registry.keyring.api.KeyModule;
import google.registry.module.RegistryComponent.RegistryModule;
import google.registry.module.RequestComponent.RequestComponentModule;
import google.registry.monitoring.whitebox.StackdriverModule;
import google.registry.mosapi.module.MosApiModule;
import google.registry.persistence.PersistenceModule;
@@ -55,7 +53,7 @@ import google.registry.util.UtilsModule;
import jakarta.inject.Provider;
import jakarta.inject.Singleton;
/** Dagger component with instance lifetime. */
/** Dagger component for the registry servlet. */
@Singleton
@Component(
modules = {
@@ -74,14 +72,13 @@ import jakarta.inject.Singleton;
GroupsModule.class,
GroupssettingsModule.class,
GsonModule.class,
MosApiModule.class,
JSchModule.class,
KeyModule.class,
KeyringModule.class,
MosApiModule.class,
NetHttpTransportModule.class,
PersistenceModule.class,
RegistryModule.class,
RequestComponentModule.class,
RegistryComponent.RegistryModule.class,
SecretManagerModule.class,
ServerTridProviderModule.class,
SheetsServiceModule.class,
@@ -90,15 +87,18 @@ import jakarta.inject.Singleton;
UtilsModule.class,
VoidDnsWriterModule.class,
})
interface RegistryComponent {
public interface RegistryComponent {
RequestHandler<RequestComponent> requestHandler();
RequestAuthenticator requestAuthenticator();
Lazy<MetricReporter> metricReporter();
@Config("projectId")
String projectId();
@Module
/** Module for {@link RegistryComponent}. */
@Module(subcomponents = RequestComponent.class)
class RegistryModule {
@Provides
static RequestHandler<RequestComponent> provideRequestHandler(
@@ -22,8 +22,6 @@ import static google.registry.util.GcpJsonFormatter.unsetLabels;
import static google.registry.util.RandomStringGenerator.insecureRandomStringGenerator;
import static google.registry.util.StringGenerator.Alphabets.HEX_DIGITS_ONLY;
import com.google.monitoring.metrics.MetricReporter;
import dagger.Lazy;
import google.registry.request.RequestHandler;
import google.registry.util.GcpJsonFormatter;
import google.registry.util.JdkLoggerConfig;
@@ -49,9 +47,8 @@ public class RegistryServlet extends ServletBase {
private static final RandomStringGenerator LOG_TRACE_ID_GENERATOR =
insecureRandomStringGenerator(HEX_DIGITS_ONLY);
private static final RegistryComponent component = DaggerRegistryComponent.create();
public static final RegistryComponent component = DaggerRegistryComponent.create();
private static final RequestHandler<RequestComponent> requestHandler = component.requestHandler();
private static final Lazy<MetricReporter> metricReporter = component.metricReporter();
// The regex pattern to capture the cookies that we want to log.
private static final Pattern COOKIE_REGEX_PATTERN =
@@ -71,7 +68,7 @@ public class RegistryServlet extends ServletBase {
}
public RegistryServlet() {
super(requestHandler, metricReporter);
super(requestHandler);
this.projectId = component.projectId();
}
@@ -15,8 +15,6 @@
package google.registry.module;
import com.google.common.flogger.FluentLogger;
import com.google.monitoring.metrics.MetricReporter;
import dagger.Lazy;
import google.registry.request.RequestHandler;
import google.registry.util.SystemClock;
import jakarta.servlet.http.HttpServlet;
@@ -26,46 +24,22 @@ import java.io.IOException;
import java.security.Security;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeoutException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
/** Base for Servlets that handle all requests to our modules. */
public class ServletBase extends HttpServlet {
private final RequestHandler<?> requestHandler;
private final Lazy<MetricReporter> metricReporter;
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final SystemClock clock = new SystemClock();
public ServletBase(RequestHandler<?> requestHandler, Lazy<MetricReporter> metricReporter) {
public ServletBase(RequestHandler<?> requestHandler) {
this.requestHandler = requestHandler;
this.metricReporter = metricReporter;
}
@Override
public void init() {
Security.addProvider(new BouncyCastleProvider());
// If the metric reporter failed to instantiate for any reason (bad keyring, bad json
// credential, etc.), we log the error but keep the main thread running. Also, the shutdown hook
// will only be registered if the metric reporter starts up correctly.
try {
metricReporter.get().startAsync().awaitRunning(Duration.ofSeconds(10));
logger.atInfo().log("Started up MetricReporter.");
Runtime.getRuntime()
.addShutdownHook(
new Thread(
() -> {
try {
metricReporter.get().stopAsync().awaitTerminated(Duration.ofSeconds(10));
logger.atInfo().log("Shut down MetricReporter.");
} catch (TimeoutException e) {
logger.atSevere().withCause(e).log("Failed to stop MetricReporter.");
}
}));
} catch (Exception e) {
logger.atSevere().withCause(e).log("Failed to initialize MetricReporter.");
}
}
@Override
@@ -0,0 +1,456 @@
// 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.
package google.registry.util;
import static java.nio.charset.StandardCharsets.UTF_8;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ReadListener;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletConnection;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpUpgradeHandler;
import jakarta.servlet.http.Part;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.security.Principal;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
/** Simple fake {@link HttpServletRequest} for internal use. */
public class FakeHttpServletRequest implements HttpServletRequest {
private final Map<String, String> headers = new HashMap<>();
private final Map<String, Object> attributes = new HashMap<>();
private String method = "POST";
private String requestUri = "/_dr/epp";
private final String serverName = "localhost";
private byte[] body = new byte[0];
private final String contentType = "application/epp+xml";
public void setHeader(String name, String value) {
headers.put(name, value);
}
public void setMethod(String method) {
this.method = method;
}
public void setRequestUri(String requestUri) {
this.requestUri = requestUri;
}
public void setBody(byte[] body) {
this.body = body.clone();
}
@Override
public String getHeader(String name) {
return headers.get(name);
}
@Override
public Enumeration<String> getHeaders(String name) {
String value = headers.get(name);
return Collections.enumeration(
value == null ? Collections.emptyList() : Collections.singletonList(value));
}
@Override
public Enumeration<String> getHeaderNames() {
return Collections.enumeration(headers.keySet());
}
@Override
public int getIntHeader(String name) {
String value = headers.get(name);
if (value == null) {
return -1;
}
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
String.format("Invalid integer value for header '%s': %s", name, value), e);
}
}
@Override
public String getMethod() {
return method;
}
@Override
public String getRequestURI() {
return requestUri;
}
@Override
public String getServerName() {
return serverName;
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public boolean isFinished() {
return byteArrayInputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return byteArrayInputStream.read(b, off, len);
}
};
}
@Override
public String getContentType() {
return contentType;
}
// --- Implement other methods with defaults ---
@Override
public String getAuthType() {
return null;
}
@Override
public Cookie[] getCookies() {
return new Cookie[0];
}
@Override
public long getDateHeader(String name) {
return -1;
}
@Override
public String getPathInfo() {
return null;
}
@Override
public String getPathTranslated() {
return null;
}
@Override
public String getContextPath() {
return "";
}
@Override
public String getQueryString() {
return null;
}
@Override
public String getRemoteUser() {
return null;
}
@Override
public boolean isUserInRole(String role) {
return false;
}
@Override
public Principal getUserPrincipal() {
return null;
}
@Override
public String getRequestedSessionId() {
return null;
}
@Override
public StringBuffer getRequestURL() {
return new StringBuffer(requestUri);
}
@Override
public String getServletPath() {
return "";
}
@Override
public HttpSession getSession(boolean create) {
return null;
}
@Override
public HttpSession getSession() {
return null;
}
@Override
public String changeSessionId() {
return null;
}
@Override
public boolean isRequestedSessionIdValid() {
return false;
}
@Override
public boolean isRequestedSessionIdFromCookie() {
return false;
}
@Override
public boolean isRequestedSessionIdFromURL() {
return false;
}
@Override
public boolean authenticate(HttpServletResponse response) throws IOException, ServletException {
return false;
}
@Override
public void login(String username, String password) throws ServletException {}
@Override
public void logout() throws ServletException {}
@Override
public Collection<Part> getParts() throws IOException, ServletException {
return null;
}
@Override
public Part getPart(String name) throws IOException, ServletException {
return null;
}
@Override
public <T extends HttpUpgradeHandler> T upgrade(Class<T> handlerClass)
throws IOException, ServletException {
return null;
}
@Override
public Object getAttribute(String name) {
return attributes.get(name);
}
@Override
public Enumeration<String> getAttributeNames() {
return Collections.enumeration(attributes.keySet());
}
@Override
public String getCharacterEncoding() {
return "UTF-8";
}
@Override
public void setCharacterEncoding(String env) throws UnsupportedEncodingException {}
@Override
public int getContentLength() {
return body.length;
}
@Override
public long getContentLengthLong() {
return body.length;
}
@Override
public String getParameter(String name) {
return null;
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.emptyEnumeration();
}
@Override
public String[] getParameterValues(String name) {
return null;
}
@Override
public Map<String, String[]> getParameterMap() {
return Collections.emptyMap();
}
@Override
public String getProtocol() {
return "HTTP/1.1";
}
@Override
public String getScheme() {
return "http";
}
@Override
public int getServerPort() {
return 8080;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(), UTF_8));
}
@Override
public String getRemoteAddr() {
return "127.0.0.1";
}
@Override
public String getRemoteHost() {
return "localhost";
}
@Override
public void setAttribute(String name, Object o) {
attributes.put(name, o);
}
@Override
public void removeAttribute(String name) {
attributes.remove(name);
}
@Override
public Locale getLocale() {
return Locale.US;
}
@Override
public Enumeration<Locale> getLocales() {
return Collections.enumeration(Collections.singleton(Locale.US));
}
@Override
public boolean isSecure() {
return false;
}
@Override
public RequestDispatcher getRequestDispatcher(String path) {
return null;
}
@Override
public int getRemotePort() {
return 0;
}
@Override
public String getLocalName() {
return "localhost";
}
@Override
public String getLocalAddr() {
return "127.0.0.1";
}
@Override
public int getLocalPort() {
return 8080;
}
@Override
public ServletContext getServletContext() {
return null;
}
@Override
public AsyncContext startAsync() throws IllegalStateException {
return null;
}
@Override
public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse)
throws IllegalStateException {
return null;
}
@Override
public boolean isAsyncStarted() {
return false;
}
@Override
public boolean isAsyncSupported() {
return false;
}
@Override
public AsyncContext getAsyncContext() {
return null;
}
@Override
public DispatcherType getDispatcherType() {
return DispatcherType.REQUEST;
}
@Override
public String getRequestId() {
return null;
}
@Override
public String getProtocolRequestId() {
return null;
}
@Override
public ServletConnection getServletConnection() {
return null;
}
}
@@ -0,0 +1,222 @@
// 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.
package google.registry.util;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.collect.ArrayListMultimap;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.WriteListener;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
/** Simple fake {@link HttpServletResponse} for internal use. */
public class FakeHttpServletResponse implements HttpServletResponse {
private int status = SC_OK;
private String contentType;
private final ArrayListMultimap<String, String> headers = ArrayListMultimap.create();
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
private final PrintWriter writer = new PrintWriter(outputStream, false, UTF_8);
@Override
public void setStatus(int sc) {
this.status = sc;
}
@Override
public int getStatus() {
return status;
}
@Override
public void setContentType(String type) {
this.contentType = type;
}
@Override
public String getContentType() {
return contentType;
}
@Override
public void setHeader(String name, String value) {
headers.removeAll(name);
headers.put(name, value);
}
@Override
public void addHeader(String name, String value) {
headers.put(name, value);
}
@Override
public String getHeader(String name) {
List<String> values = headers.get(name);
return values.isEmpty() ? null : values.get(0);
}
@Override
public Collection<String> getHeaders(String name) {
return headers.get(name);
}
@Override
public Collection<String> getHeaderNames() {
return headers.keySet();
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new ServletOutputStream() {
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {}
@Override
public void write(int b) throws IOException {
outputStream.write(b);
}
};
}
@Override
public PrintWriter getWriter() throws IOException {
return writer;
}
public byte[] getPayload() {
writer.flush();
return outputStream.toByteArray();
}
// --- Implement other methods with defaults ---
@Override
public void addCookie(Cookie cookie) {}
@Override
public boolean containsHeader(String name) {
return headers.containsKey(name);
}
@Override
public String encodeURL(String url) {
return url;
}
@Override
public String encodeRedirectURL(String url) {
return url;
}
@Override
public void sendError(int sc, String msg) throws IOException {
this.status = sc;
}
@Override
public void sendError(int sc) throws IOException {
this.status = sc;
}
@Override
public void sendRedirect(String location) throws IOException {
this.status = SC_FOUND;
}
@Override
public void sendRedirect(String location, int sc, boolean clearBuffer) throws IOException {
this.status = sc;
}
@Override
public void setDateHeader(String name, long date) {}
@Override
public void addDateHeader(String name, long date) {}
@Override
public void setIntHeader(String name, int value) {}
@Override
public void addIntHeader(String name, int value) {}
@Override
public void setCharacterEncoding(String charset) {}
@Override
public void setContentLength(int len) {}
@Override
public void setContentLengthLong(long len) {}
@Override
public void setBufferSize(int size) {}
@Override
public int getBufferSize() {
return 0;
}
@Override
public void flushBuffer() throws IOException {
writer.flush();
}
@Override
public void resetBuffer() {
writer.flush();
outputStream.reset();
}
@Override
public boolean isCommitted() {
return false;
}
@Override
public void reset() {
writer.flush();
outputStream.reset();
headers.clear();
status = SC_OK;
}
@Override
public void setLocale(Locale loc) {}
@Override
public Locale getLocale() {
return Locale.US;
}
@Override
public String getCharacterEncoding() {
return "UTF-8";
}
@Override
public void sendEarlyHints() {}
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<hello/>
</epp>
@@ -0,0 +1,75 @@
// 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.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
class EppProxyProtocolHandlerTest {
@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_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 = new EmbeddedChannel(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();
// In EmbeddedChannel without remoteAddress mock, getSourceIP returns null
assertThat(remoteAddress).isNull();
assertThat(channel.pipeline().get(EppProxyProtocolHandler.class)).isNull();
ByteBuf passedOn = channel.readInbound();
assertThat(passedOn.toString(StandardCharsets.US_ASCII)).isEqualTo("NOT_A_PROXY_HEADER");
}
}
@@ -0,0 +1,297 @@
// 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.
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 java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import google.registry.eppserver.metric.FrontendMetrics;
import google.registry.eppserver.quota.LocalConnectionLimiter;
import google.registry.eppserver.quota.QuotaManager;
import google.registry.eppserver.quota.QuotaManager.QuotaRequest;
import google.registry.eppserver.quota.QuotaManager.QuotaResponse;
import google.registry.request.RequestHandler;
import google.registry.util.FakeHttpServletRequest;
import google.registry.util.FakeHttpServletResponse;
import google.registry.util.ProxyHttpHeaders;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.util.Attribute;
import io.netty.util.concurrent.DefaultPromise;
import io.netty.util.concurrent.ImmediateEventExecutor;
import io.netty.util.concurrent.Promise;
import java.security.cert.X509Certificate;
import java.util.function.Supplier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class EppServiceHandlerTest {
@Mock private FrontendMetrics metrics;
@Mock private LocalConnectionLimiter localConnectionLimiter;
@Mock private QuotaManager commandQuotaManager;
@Mock private Supplier<String> idTokenSupplier;
@Mock private ChannelHandlerContext ctx;
@Mock private Channel channel;
@Mock private RequestHandler<?> requestHandler;
@Mock private Attribute<Promise<X509Certificate>> certPromiseAttr;
@Mock private Attribute<String> remoteAddressAttr;
@Mock private Attribute<String> certHashAttr;
@Mock private X509Certificate certificate;
private EppServiceHandler handler;
private DefaultPromise<X509Certificate> certPromise;
@BeforeEach
void setUp() {
handler =
new EppServiceHandler(
new byte[] {'h', 'e', 'l', 'l', 'o'},
metrics,
localConnectionLimiter,
commandQuotaManager,
idTokenSupplier,
"test-project");
handler.requestHandler = requestHandler;
when(ctx.channel()).thenReturn(channel);
when(ctx.executor()).thenReturn(ImmediateEventExecutor.INSTANCE);
}
private void setUpSuccessfulHandshake() throws Exception {
certPromise = new DefaultPromise<>(ImmediateEventExecutor.INSTANCE);
when(channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY)).thenReturn(certPromiseAttr);
when(certPromiseAttr.get()).thenReturn(certPromise);
handler.channelActive(ctx);
when(channel.attr(REMOTE_ADDRESS_KEY)).thenReturn(remoteAddressAttr);
when(remoteAddressAttr.get()).thenReturn("192.168.1.1");
when(channel.attr(EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY)).thenReturn(certHashAttr);
when(certificate.getEncoded()).thenReturn(new byte[] {1, 2, 3});
when(localConnectionLimiter.acquireIp(any(String.class))).thenReturn(true);
when(localConnectionLimiter.acquireCert(any(String.class))).thenReturn(true);
certPromise.setSuccess(certificate);
}
@Test
void testChannelActive_success() throws Exception {
doAnswer(
invocation -> {
FakeHttpServletResponse rsp = invocation.getArgument(1);
rsp.getWriter().write("<epp><greeting/></epp>");
return null;
})
.when(requestHandler)
.handleRequest(any(FakeHttpServletRequest.class), any(FakeHttpServletResponse.class));
when(commandQuotaManager.acquireQuota(any(QuotaRequest.class)))
.thenReturn(new QuotaResponse(true));
when(idTokenSupplier.get()).thenReturn("fake_id_token");
setUpSuccessfulHandshake();
verify(metrics).registerActiveConnection(eq("epp"), any(String.class), eq(channel));
// Verify the initial <greeting> frame is sent back to the client
verify(ctx)
.writeAndFlush(
argThat(
(ByteBuf buf) -> {
String xml = buf.toString(UTF_8);
return xml.equals("<epp><greeting/></epp>");
}));
}
@Test
void testChannelActive_ipQuotaRejected() throws Exception {
certPromise = new DefaultPromise<>(ImmediateEventExecutor.INSTANCE);
when(channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY)).thenReturn(certPromiseAttr);
when(certPromiseAttr.get()).thenReturn(certPromise);
handler.channelActive(ctx);
when(channel.attr(REMOTE_ADDRESS_KEY)).thenReturn(remoteAddressAttr);
when(remoteAddressAttr.get()).thenReturn("192.168.1.1");
when(channel.attr(EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY)).thenReturn(certHashAttr);
when(certificate.getEncoded()).thenReturn(new byte[] {1, 2, 3});
when(localConnectionLimiter.acquireIp(any(String.class))).thenReturn(false);
certPromise.setSuccess(certificate);
verify(metrics).registerQuotaRejection(eq("epp_connection_ip"), eq("192.168.1.1"));
verify(ctx).close();
}
@Test
void testChannelActive_certQuotaRejected() throws Exception {
certPromise = new DefaultPromise<>(ImmediateEventExecutor.INSTANCE);
when(channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY)).thenReturn(certPromiseAttr);
when(certPromiseAttr.get()).thenReturn(certPromise);
handler.channelActive(ctx);
when(channel.attr(REMOTE_ADDRESS_KEY)).thenReturn(remoteAddressAttr);
when(remoteAddressAttr.get()).thenReturn("192.168.1.1");
when(channel.attr(EppServiceHandler.CLIENT_CERTIFICATE_HASH_KEY)).thenReturn(certHashAttr);
when(certificate.getEncoded()).thenReturn(new byte[] {1, 2, 3});
when(localConnectionLimiter.acquireIp(any(String.class))).thenReturn(true);
when(localConnectionLimiter.acquireCert(any(String.class))).thenReturn(false);
certPromise.setSuccess(certificate);
verify(metrics).registerQuotaRejection(eq("epp_connection"), any(String.class));
verify(ctx).close();
}
@Test
void testChannelRead0_extractsClidAndForwardsRequest() throws Exception {
setUpSuccessfulHandshake();
when(idTokenSupplier.get()).thenReturn("fake_id_token");
when(commandQuotaManager.acquireQuota(any(QuotaRequest.class)))
.thenReturn(new QuotaResponse(true));
String eppLoginXml = "<epp><command><login><clID>RegistrarA</clID></login></command></epp>";
ByteBuf inFrame = Unpooled.wrappedBuffer(eppLoginXml.getBytes(UTF_8));
// Simulate FakeHttpServletResponse modifying headers and writing payload
doAnswer(
invocation -> {
FakeHttpServletRequest req = invocation.getArgument(0);
FakeHttpServletResponse rsp = invocation.getArgument(1);
rsp.setHeader("Set-Cookie", "SESSION_INFO=xyz123");
rsp.getWriter().write("<epp><response>success</response></epp>");
return null;
})
.when(requestHandler)
.handleRequest(any(FakeHttpServletRequest.class), any(FakeHttpServletResponse.class));
handler.channelRead0(ctx, inFrame);
// Verify command quota was requested for the extracted clID "RegistrarA"
verify(commandQuotaManager).acquireQuota(eq(new QuotaRequest("RegistrarA")));
// Verify the response from the servlet was written back to the channel
verify(ctx)
.writeAndFlush(
argThat(
(ByteBuf buf) -> {
String xml = buf.toString(UTF_8);
return xml.equals("<epp><response>success</response></epp>");
}));
// On the next request, the cookie should be injected
String eppCommandXml = "<epp><command><check></check></command></epp>";
ByteBuf inFrame2 = Unpooled.wrappedBuffer(eppCommandXml.getBytes(UTF_8));
doAnswer(
invocation -> {
FakeHttpServletRequest req = invocation.getArgument(0);
// Verify the cookie was properly propagated
if (!"SESSION_INFO=xyz123".equals(req.getHeader("Cookie"))) {
throw new AssertionError("Missing or incorrect cookie");
}
// Verify the registrar ID was properly propagated
if (!"RegistrarA".equals(req.getHeader(ProxyHttpHeaders.REGISTRAR_ID))) {
throw new AssertionError("Missing or incorrect registrar ID");
}
return null;
})
.when(requestHandler)
.handleRequest(any(FakeHttpServletRequest.class), any(FakeHttpServletResponse.class));
handler.channelRead0(ctx, inFrame2);
}
@Test
void testChannelRead0_commandQuotaRejected() throws Exception {
setUpSuccessfulHandshake();
when(commandQuotaManager.acquireQuota(any(QuotaRequest.class)))
.thenReturn(new QuotaResponse(false));
String eppXml = "<epp><command><check></check></command></epp>";
ByteBuf inFrame = Unpooled.wrappedBuffer(eppXml.getBytes(UTF_8));
handler.channelRead0(ctx, inFrame);
verify(metrics).registerQuotaRejection(eq("epp_command"), any(String.class));
verify(ctx).close();
}
@Test
void testChannelRead0_closeSessionHeaderClosesChannel() throws Exception {
setUpSuccessfulHandshake();
when(idTokenSupplier.get()).thenReturn("fake_id_token");
when(commandQuotaManager.acquireQuota(any(QuotaRequest.class)))
.thenReturn(new QuotaResponse(true));
String eppLogoutXml = "<epp><command><logout/></command></epp>";
ByteBuf inFrame = Unpooled.wrappedBuffer(eppLogoutXml.getBytes(UTF_8));
ChannelFuture mockFuture = mock(ChannelFuture.class);
when(ctx.writeAndFlush(any(ByteBuf.class))).thenReturn(mockFuture);
doAnswer(
invocation -> {
FakeHttpServletResponse rsp = invocation.getArgument(1);
rsp.setHeader(ProxyHttpHeaders.EPP_SESSION, "close");
rsp.getWriter().write("<epp><response>logout success</response></epp>");
return null;
})
.when(requestHandler)
.handleRequest(any(FakeHttpServletRequest.class), any(FakeHttpServletResponse.class));
handler.channelRead0(ctx, inFrame);
// Verify the handler added the CLOSE listener to the flush future
verify(mockFuture).addListener(ChannelFutureListener.CLOSE);
}
@Test
void testChannelInactive_releasesQuotas() throws Exception {
setUpSuccessfulHandshake();
handler.channelInactive(ctx);
// Verify the in-memory limiter releases both IP and Cert
verify(localConnectionLimiter).releaseIp(eq("192.168.1.1"));
verify(localConnectionLimiter).releaseCert(any(String.class));
}
}
@@ -0,0 +1,52 @@
// 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.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
class HealthCheckHandlerTest {
@Test
void testHealthCheck_success() {
HealthCheckHandler handler = new HealthCheckHandler("ping", "pong");
EmbeddedChannel channel = new EmbeddedChannel(handler);
ByteBuf pingMsg = Unpooled.wrappedBuffer("ping".getBytes(StandardCharsets.US_ASCII));
channel.writeInbound(pingMsg);
ByteBuf response = channel.readOutbound();
assertThat(response.toString(StandardCharsets.US_ASCII)).isEqualTo("pong");
assertThat(channel.isOpen()).isTrue();
}
@Test
void testHealthCheck_ignoresOtherMessages() {
HealthCheckHandler handler = new HealthCheckHandler("ping", "pong");
EmbeddedChannel channel = new EmbeddedChannel(handler);
ByteBuf otherMsg = Unpooled.wrappedBuffer("foo".getBytes(StandardCharsets.US_ASCII));
channel.writeInbound(otherMsg);
ByteBuf response = channel.readOutbound();
assertThat(response).isNull();
assertThat(channel.isOpen()).isTrue();
}
}
@@ -0,0 +1,101 @@
// 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.
package google.registry.eppserver.quota;
import static com.google.common.truth.Truth.assertThat;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
class LocalConnectionLimiterTest {
private LocalConnectionLimiter limiter;
@BeforeEach
void setUp() {
// Set a small limit of 2 for testing purposes.
limiter = new LocalConnectionLimiter(2, 2);
}
@Test
void testAcquireIp_successUpToLimit() {
assertThat(limiter.acquireIp("192.168.1.1")).isTrue();
assertThat(limiter.acquireIp("192.168.1.1")).isTrue();
}
@Test
void testAcquireIp_rejectsOverLimit() {
assertThat(limiter.acquireIp("192.168.1.1")).isTrue();
assertThat(limiter.acquireIp("192.168.1.1")).isTrue();
// 3rd attempt from same IP should be rejected
assertThat(limiter.acquireIp("192.168.1.1")).isFalse();
}
@Test
void testAcquireIp_independentAcrossIps() {
assertThat(limiter.acquireIp("192.168.1.1")).isTrue();
assertThat(limiter.acquireIp("192.168.1.1")).isTrue();
assertThat(limiter.acquireIp("192.168.1.1")).isFalse();
// A different IP should still be allowed
assertThat(limiter.acquireIp("10.0.0.1")).isTrue();
}
@Test
void testReleaseIp_freesSlot() {
assertThat(limiter.acquireIp("192.168.1.1")).isTrue();
assertThat(limiter.acquireIp("192.168.1.1")).isTrue();
assertThat(limiter.acquireIp("192.168.1.1")).isFalse();
limiter.releaseIp("192.168.1.1");
// Now we should be able to acquire again
assertThat(limiter.acquireIp("192.168.1.1")).isTrue();
}
@Test
void testAcquireCert_successUpToLimit() {
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
}
@Test
void testAcquireCert_rejectsOverLimit() {
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
// 3rd attempt from same cert should be rejected
assertThat(limiter.acquireCert("cert_hash_1")).isFalse();
}
@Test
void testAcquireCert_independentAcrossCerts() {
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
assertThat(limiter.acquireCert("cert_hash_1")).isFalse();
// A different cert should still be allowed
assertThat(limiter.acquireCert("cert_hash_2")).isTrue();
}
@Test
void testReleaseCert_freesSlot() {
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
assertThat(limiter.acquireCert("cert_hash_1")).isFalse();
limiter.releaseCert("cert_hash_1");
// Now we should be able to acquire again
assertThat(limiter.acquireCert("cert_hash_1")).isTrue();
}
}
@@ -0,0 +1,159 @@
// 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.quota;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import google.registry.config.RegistryConfigSettings.Quota;
import google.registry.config.RegistryConfigSettings.Quota.QuotaGroup;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import redis.clients.jedis.UnifiedJedis;
@ExtendWith(MockitoExtension.class)
class QuotaManagerTest {
@Mock private UnifiedJedis jedis;
private Quota quotaConfig;
private QuotaManager manager;
@BeforeEach
void setUp() {
quotaConfig = new Quota();
QuotaGroup defaultGroup = new QuotaGroup();
defaultGroup.tokenAmount = 10;
defaultGroup.refillSeconds = 60;
quotaConfig.defaultQuota = defaultGroup;
QuotaGroup customGroup = new QuotaGroup();
customGroup.tokenAmount = 5;
customGroup.refillSeconds = 30;
customGroup.userId = ImmutableList.of("user1");
quotaConfig.customQuota = ImmutableList.of(customGroup);
manager = new QuotaManager(quotaConfig, jedis, "testQuota");
}
@Test
void testAcquireQuota_success() {
when(jedis.eval(anyString(), anyInt(), anyString(), anyString(), anyString())).thenReturn(5L);
QuotaManager.QuotaResponse response =
manager.acquireQuota(new QuotaManager.QuotaRequest("user2"));
assertThat(response.success()).isTrue();
verify(jedis).eval(anyString(), eq(1), eq("testQuota:user2"), eq("10"), eq("60"));
}
@Test
void testAcquireQuota_failure() {
when(jedis.eval(anyString(), anyInt(), anyString(), anyString(), anyString())).thenReturn(-1L);
QuotaManager.QuotaResponse response =
manager.acquireQuota(new QuotaManager.QuotaRequest("user1"));
assertThat(response.success()).isFalse();
verify(jedis).eval(anyString(), eq(1), eq("testQuota:user1"), eq("5"), eq("30"));
}
@Test
void testAcquireQuota_unlimited() {
quotaConfig.defaultQuota.tokenAmount = -1;
manager = new QuotaManager(quotaConfig, jedis, "testQuota");
QuotaManager.QuotaResponse response =
manager.acquireQuota(new QuotaManager.QuotaRequest("user2"));
assertThat(response.success()).isTrue();
}
@Test
void testAcquireQuota_jedisException_failsOpen() {
when(jedis.eval(anyString(), anyInt(), anyString(), anyString(), anyString()))
.thenThrow(new RuntimeException("Redis error"));
QuotaManager.QuotaResponse response =
manager.acquireQuota(new QuotaManager.QuotaRequest("user2"));
assertThat(response.success()).isTrue();
}
@Test
void testRefreshQuota_success() {
manager.refreshQuota(new QuotaManager.QuotaRequest("user2"));
verify(jedis).eval(anyString(), eq(1), eq("testQuota:user2"), eq("60"));
}
@Test
void testReleaseQuota_success() {
manager.releaseQuota(new QuotaManager.QuotaRebate("user2"));
verify(jedis).eval(anyString(), eq(1), eq("testQuota:user2"), eq("10"));
}
@Test
void testGroupVirtualIdentity_usesFirstIdInList() {
// Modify config so "user1" is accompanied by a virtual group ID "my_group"
quotaConfig.customQuota.get(0).userId = ImmutableList.of("my_group", "user1", "user3");
manager = new QuotaManager(quotaConfig, jedis, "testQuota");
when(jedis.eval(anyString(), anyInt(), anyString(), anyString(), anyString())).thenReturn(5L);
QuotaManager.QuotaResponse response1 =
manager.acquireQuota(new QuotaManager.QuotaRequest("user1"));
QuotaManager.QuotaResponse response2 =
manager.acquireQuota(new QuotaManager.QuotaRequest("user3"));
assertThat(response1.success()).isTrue();
assertThat(response2.success()).isTrue();
// 5 tokens, 30 seconds ttl
verify(jedis, times(2)).eval(anyString(), eq(1), eq("testQuota:my_group"), eq("5"), eq("30"));
}
@Test
void testGroupVirtualIdentity_exceedsQuota_fails() {
// Modify config so "user1", "user2", "user3" share virtual group ID "my_group"
quotaConfig.customQuota.get(0).userId = ImmutableList.of("my_group", "user1", "user2", "user3");
manager = new QuotaManager(quotaConfig, jedis, "testQuota");
// Simulate Redis returning 1, 0 for successful decrements, and -1 when empty
when(jedis.eval(anyString(), anyInt(), anyString(), anyString(), anyString()))
.thenReturn(1L)
.thenReturn(0L)
.thenReturn(-1L);
// Act
QuotaManager.QuotaResponse response1 =
manager.acquireQuota(new QuotaManager.QuotaRequest("user1"));
QuotaManager.QuotaResponse response2 =
manager.acquireQuota(new QuotaManager.QuotaRequest("user2"));
QuotaManager.QuotaResponse response3 =
manager.acquireQuota(new QuotaManager.QuotaRequest("user3"));
// Assert that the third request to the same group fails
assertThat(response1.success()).isTrue();
assertThat(response2.success()).isTrue();
assertThat(response3.success()).isFalse();
// Verify all 3 requests went to the shared bucket
verify(jedis, times(3)).eval(anyString(), eq(1), eq("testQuota:my_group"), eq("5"), eq("30"));
}
}
@@ -14,9 +14,6 @@
package google.registry.module;
import com.google.monitoring.metrics.MetricReporter;
import dagger.Lazy;
/**
* Servlet used in the test server to handle routing.
*
@@ -27,9 +24,8 @@ import dagger.Lazy;
public class TestServlet extends ServletBase {
private static final TestRegistryComponent component = DaggerTestRegistryComponent.create();
private static final TestRequestHandler requestHandler = component.requestHandler();
private static final Lazy<MetricReporter> metricReporter = component.metricReporter();
public TestServlet() {
super(requestHandler, metricReporter);
super(requestHandler);
}
}
@@ -0,0 +1,78 @@
// 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.util;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link FakeHttpServletRequest}. */
class FakeHttpServletRequestTest {
private FakeHttpServletRequest request;
@BeforeEach
void setUp() {
request = new FakeHttpServletRequest();
}
@Test
void testHeaders() {
request.setHeader("X-My-Header", "Value1");
assertThat(request.getHeader("X-My-Header")).isEqualTo("Value1");
assertThat(Collections.list(request.getHeaders("X-My-Header"))).containsExactly("Value1");
assertThat(Collections.list(request.getHeaderNames())).containsExactly("X-My-Header");
}
@Test
void testIntHeader() {
request.setHeader("X-Int-Header", "42");
assertThat(request.getIntHeader("X-Int-Header")).isEqualTo(42);
assertThat(request.getIntHeader("Missing")).isEqualTo(-1);
}
@Test
void testMethodAndUri() {
request.setMethod("GET");
request.setRequestUri("/test/path");
assertThat(request.getMethod()).isEqualTo("GET");
assertThat(request.getRequestURI()).isEqualTo("/test/path");
}
@Test
void testBodyAndReader() throws IOException {
String payload = "test body";
request.setBody(payload.getBytes(UTF_8));
BufferedReader reader = request.getReader();
assertThat(reader.readLine()).isEqualTo(payload);
assertThat(request.getContentLength()).isEqualTo(payload.length());
}
@Test
void testAttributes() {
request.setAttribute("attr1", "val1");
assertThat(request.getAttribute("attr1")).isEqualTo("val1");
assertThat(Collections.list(request.getAttributeNames())).containsExactly("attr1");
request.removeAttribute("attr1");
assertThat(request.getAttribute("attr1")).isNull();
}
}
@@ -0,0 +1,117 @@
// 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.util;
import static com.google.common.truth.Truth.assertThat;
import static jakarta.servlet.http.HttpServletResponse.SC_FOUND;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.IOException;
import java.io.PrintWriter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link FakeHttpServletResponse}. */
class FakeHttpServletResponseTest {
private FakeHttpServletResponse response;
@BeforeEach
void setUp() {
response = new FakeHttpServletResponse();
}
@Test
void testStatus() throws IOException {
assertThat(response.getStatus()).isEqualTo(SC_OK);
response.setStatus(400);
assertThat(response.getStatus()).isEqualTo(400);
response.sendError(500);
assertThat(response.getStatus()).isEqualTo(500);
response.sendRedirect("/redirect");
assertThat(response.getStatus()).isEqualTo(SC_FOUND);
}
@Test
void testContentType() {
response.setContentType("text/plain");
assertThat(response.getContentType()).isEqualTo("text/plain");
}
@Test
void testHeaders() {
response.setHeader("X-Test", "Val");
assertThat(response.getHeader("X-Test")).isEqualTo("Val");
assertThat(response.getHeaders("X-Test")).containsExactly("Val");
assertThat(response.getHeaderNames()).containsExactly("X-Test");
assertThat(response.containsHeader("X-Test")).isTrue();
}
@Test
void testMultipleHeaders() {
response.setHeader("X-Test", "Val1");
response.addHeader("X-Test", "Val2");
assertThat(response.getHeader("X-Test")).isEqualTo("Val1");
assertThat(response.getHeaders("X-Test")).containsExactly("Val1", "Val2");
response.setHeader("X-Test", "Val3");
assertThat(response.getHeader("X-Test")).isEqualTo("Val3");
assertThat(response.getHeaders("X-Test")).containsExactly("Val3");
}
@Test
void testWriterAndPayload() throws IOException {
PrintWriter writer = response.getWriter();
writer.print("hello world");
assertThat(new String(response.getPayload(), UTF_8)).isEqualTo("hello world");
}
@Test
void testOutputStreamAndPayload() throws IOException {
byte[] data = "test bytes".getBytes(UTF_8);
response.getOutputStream().write(data);
assertThat(response.getPayload()).isEqualTo(data);
}
@Test
void testReset() throws IOException {
response.setStatus(404);
response.setHeader("A", "B");
response.getWriter().print("content");
response.reset();
assertThat(response.getStatus()).isEqualTo(SC_OK);
assertThat(response.getHeaderNames()).isEmpty();
assertThat(response.getPayload()).isEmpty();
}
@Test
void testSendRedirectWithStatus() throws IOException {
response.sendRedirect("/redirect", 301, false);
assertThat(response.getStatus()).isEqualTo(301);
}
@Test
void testSendEarlyHints() {
response.sendEarlyHints(); // Should not throw
}
}
+5 -5
View File
@@ -4,9 +4,9 @@
com.fasterxml.jackson.core:jackson-annotations:2.21=classpath
gradle.plugin.org.flywaydb:gradle-plugin-publishing:12.2.0=classpath
org.flywaydb.flyway:org.flywaydb.flyway.gradle.plugin:12.2.0=classpath
org.flywaydb:flyway-core:12.11.0=classpath
org.flywaydb:flyway-database-postgresql:12.11.0=classpath
tools.jackson.core:jackson-core:3.1.4=classpath
tools.jackson.core:jackson-databind:3.1.4=classpath
tools.jackson:jackson-bom:3.1.4=classpath
org.flywaydb:flyway-core:13.0.0=classpath
org.flywaydb:flyway-database-postgresql:13.0.0=classpath
tools.jackson.core:jackson-core:3.1.5=classpath
tools.jackson.core:jackson-databind:3.1.5=classpath
tools.jackson:jackson-bom:3.1.5=classpath
empty=
+5 -5
View File
@@ -111,8 +111,8 @@ org.codehaus.plexus:plexus-classworlds:2.6.0=checkstyle
org.codehaus.plexus:plexus-component-annotations:2.1.0=checkstyle
org.codehaus.plexus:plexus-container-default:2.1.0=checkstyle
org.codehaus.plexus:plexus-utils:3.3.0=checkstyle
org.flywaydb:flyway-core:12.11.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-database-postgresql:12.11.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:13.0.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.flywaydb:flyway-database-postgresql:13.0.0=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
org.freemarker:freemarker:2.3.34=testCompileClasspath,testRuntimeClasspath
org.hamcrest:hamcrest-core:1.3=testCompileClasspath,testRuntimeClasspath
org.jacoco:org.jacoco.agent:0.8.14=jacocoAgent,jacocoAnt
@@ -151,7 +151,7 @@ org.testcontainers:postgresql:1.21.4=testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers:1.21.4=testCompileClasspath,testRuntimeClasspath
org.testcontainers:testcontainers:2.0.5=compileClasspath
org.xmlresolver:xmlresolver:5.2.2=checkstyle
tools.jackson.core:jackson-core:3.1.4=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson.core:jackson-databind:3.1.4=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson:jackson-bom:3.1.4=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson.core:jackson-core:3.1.5=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson.core:jackson-databind:3.1.5=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
tools.jackson:jackson-bom:3.1.5=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
empty=implementationApi,integration,schema,shadow
+1 -1
View File
@@ -33,7 +33,7 @@ line=$(gcloud container clusters list --project "${project}" | grep nomulus | gr
parts=(${line})
echo "Updating cluster ${parts[0]} in location ${parts[1]}..."
gcloud container fleet memberships get-credentials "${parts[0]}" --project "${project}"
for service in frontend backend pubapi console
for service in frontend backend pubapi console epp-server
do
sed s/GCP_PROJECT/"${project}"/g "./kubernetes/nomulus-${service}.yaml" | \
sed s/ENVIRONMENT/"${environment}"/g | \
+24 -19
View File
@@ -72,10 +72,11 @@ com.google.api:gax-httpjson:2.82.0=deploy_jar,runtimeClasspath,testRuntimeClassp
com.google.api:gax:2.82.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
com.google.apis:google-api-services-admin-directory:directory_v1-rev20260531-2.0.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
com.google.apis:google-api-services-bigquery:v2-rev20251012-2.0.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
com.google.apis:google-api-services-cloudkms:v1-rev20260702-2.0.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
com.google.apis:google-api-services-cloudresourcemanager:v1-rev20250606-2.0.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
com.google.apis:google-api-services-dataflow:v1b3-rev20260615-2.0.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
com.google.apis:google-api-services-dns:v1-rev20260616-2.0.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
com.google.apis:google-api-services-drive:v3-rev20260624-2.0.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
com.google.apis:google-api-services-drive:v3-rev20260712-2.0.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
com.google.apis:google-api-services-gmail:v1-rev20260525-2.0.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
com.google.apis:google-api-services-groupssettings:v1-rev20220614-2.0.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
com.google.apis:google-api-services-healthcare:v1-rev20240130-2.0.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
@@ -218,19 +219,23 @@ io.grpc:grpc-stub:1.81.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.grpc:grpc-testing:1.70.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.grpc:grpc-util:1.81.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.grpc:grpc-xds:1.81.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-buffer:4.1.132.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-buffer:4.2.16.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-base:4.2.16.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-compression:4.2.16.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-http2:4.1.132.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-http:4.1.132.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-http:4.2.16.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-marshalling:4.2.16.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-protobuf:4.2.16.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec-socks:4.1.132.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec:4.1.132.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-common:4.1.132.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-codec:4.2.16.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-common:4.2.16.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-handler-proxy:4.1.132.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-handler:4.1.132.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-resolver:4.1.132.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-handler:4.2.16.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-resolver:4.2.16.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-tcnative-boringssl-static:2.0.52.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-tcnative-classes:2.0.52.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-transport-native-unix-common:4.1.132.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-transport:4.1.132.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-transport-native-unix-common:4.2.16.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-transport:4.2.16.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.opencensus:opencensus-api:0.31.1=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.opencensus:opencensus-contrib-exemplar-util:0.31.1=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.opencensus:opencensus-contrib-grpc-metrics:0.31.1=deploy_jar,runtimeClasspath,testRuntimeClasspath
@@ -339,8 +344,8 @@ org.codehaus.woodstox:stax2-api:4.2.2=deploy_jar,runtimeClasspath,testRuntimeCla
org.conscrypt:conscrypt-openjdk-uber:2.5.2=deploy_jar,runtimeClasspath,testRuntimeClasspath
org.eclipse.angus:angus-activation:2.1.0-M1=deploy_jar,runtimeClasspath,testRuntimeClasspath
org.eclipse.angus:jakarta.mail:2.1.0-M1=deploy_jar,runtimeClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:12.11.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
org.flywaydb:flyway-database-postgresql:12.11.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
org.flywaydb:flyway-core:13.0.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
org.flywaydb:flyway-database-postgresql:13.0.0=deploy_jar,runtimeClasspath,testRuntimeClasspath
org.freemarker:freemarker:2.3.34=deploy_jar,runtimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:jaxb-core:4.0.6=deploy_jar,runtimeClasspath,testRuntimeClasspath
org.glassfish.jaxb:jaxb-runtime:4.0.6=deploy_jar,runtimeClasspath,testRuntimeClasspath
@@ -397,16 +402,16 @@ org.xmlresolver:xmlresolver:5.2.2=checkstyle
org.yaml:snakeyaml:2.5=deploy_jar,runtimeClasspath,testRuntimeClasspath
redis.clients.authentication:redis-authx-core:0.1.1-beta2=deploy_jar,runtimeClasspath,testRuntimeClasspath
redis.clients:jedis:8.0.0-beta1=deploy_jar,runtimeClasspath,testRuntimeClasspath
tools.jackson.core:jackson-core:3.1.4=deploy_jar,runtimeClasspath,testRuntimeClasspath
tools.jackson.core:jackson-databind:3.1.4=deploy_jar,runtimeClasspath,testRuntimeClasspath
tools.jackson:jackson-bom:3.1.4=deploy_jar,runtimeClasspath,testRuntimeClasspath
tools.jackson.core:jackson-core:3.1.5=deploy_jar,runtimeClasspath,testRuntimeClasspath
tools.jackson.core:jackson-databind:3.1.5=deploy_jar,runtimeClasspath,testRuntimeClasspath
tools.jackson:jackson-bom:3.1.5=deploy_jar,runtimeClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-api:17.1.7=deploy_jar,runtimeClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-diagram:17.12.1=deploy_jar,runtimeClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-operations:17.12.1=deploy_jar,runtimeClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-postgresql:17.12.1=deploy_jar,runtimeClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-text:17.12.1=deploy_jar,runtimeClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-diagram:17.12.2=deploy_jar,runtimeClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-operations:17.12.2=deploy_jar,runtimeClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-postgresql:17.12.2=deploy_jar,runtimeClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-text:17.12.2=deploy_jar,runtimeClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-tools:17.1.7=deploy_jar,runtimeClasspath,testRuntimeClasspath
us.fatehi:schemacrawler-utility:17.1.7=deploy_jar,runtimeClasspath,testRuntimeClasspath
us.fatehi:schemacrawler:17.12.1=deploy_jar,runtimeClasspath,testRuntimeClasspath
us.fatehi:schemacrawler:17.12.2=deploy_jar,runtimeClasspath,testRuntimeClasspath
xerces:xmlParserAPIs:2.6.2=deploy_jar,runtimeClasspath,testRuntimeClasspath
empty=compileClasspath,providedCompile,providedRuntime,shadow,testCompileClasspath
+105
View File
@@ -0,0 +1,105 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: epp-server
annotations:
tag: "latest"
spec:
selector:
matchLabels:
service: epp-server
template:
metadata:
labels:
service: epp-server
spec:
serviceAccountName: nomulus
nodeSelector:
cloud.google.com/machine-family: c4
containers:
- name: frontend
image: gcr.io/GCP_PROJECT/nomulus
ports:
- containerPort: 8080
name: http
- containerPort: 30002
name: epp
- containerPort: 30000
name: health-check
readinessProbe:
tcpSocket:
port: health-check
initialDelaySeconds: 15
periodSeconds: 10
resources:
requests:
# explicit pod-slots 0 is required in order to downgrade node
# class from performance, which has implicit pod-slots 1
cloud.google.com/pod-slots: 0
cpu: "1200m"
memory: "3Gi"
limits:
# explicit pod-slots 0 is required in order to downgrade node
# class from performance, which has implicit pod-slots 1
cloud.google.com/pod-slots: 0
cpu: "2000m"
memory: "6Gi"
args: [ENVIRONMENT]
env:
- name: TCP_SERVER_ENABLED
value: "true"
- name: POD_ID
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: JETTY_WORKER_INSTANCE
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NAMESPACE_ID
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: CONTAINER_NAME
value: frontend
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: epp-server
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: epp-server
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80
---
apiVersion: v1
kind: Service
metadata:
name: epp-server
annotations:
cloud.google.com/l4-rbs: enabled
networking.gke.io/weighted-load-balancing: pods-per-node
# MUST RESERVE THESE NEW IPS IN GCP BEFORE DEPLOYING
networking.gke.io/load-balancer-ip-addresses: "EPP-v2-ipv6-main,EPP-v2-ipv4-main"
spec:
type: LoadBalancer
# Traffic is directly delivered to a node, preserving the original source IP.
externalTrafficPolicy: Local
ipFamilies: [IPv4, IPv6]
ipFamilyPolicy: RequireDualStack
selector:
service: epp-server
ports:
- port: 700
targetPort: epp
name: epp
+8
View File
@@ -2,6 +2,14 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">
<!-- Listeners -->
<listener>
<listener-class>google.registry.module.MetricsLifecycleListener</listener-class>
</listener>
<listener>
<listener-class>google.registry.module.EppServerLifecycleListener</listener-class>
</listener>
<!-- Filters -->
<filter>
<filter-name>CspFilter</filter-name>
+2 -2
View File
@@ -98,8 +98,8 @@ io.netty:netty-codec:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,t
io.netty:netty-common:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-handler:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-resolver:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-tcnative-boringssl-static:2.0.80.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-tcnative-classes:2.0.80.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-tcnative-boringssl-static:2.0.81.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-tcnative-classes:2.0.81.Final=deploy_jar,runtimeClasspath,testRuntimeClasspath
io.netty:netty-transport-native-unix-common:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-transport:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opencensus:opencensus-api:0.31.1=deploy_jar,runtimeClasspath,testRuntimeClasspath
+2 -2
View File
@@ -102,8 +102,8 @@ io.netty:netty-codec:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,t
io.netty:netty-common:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-handler:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-resolver:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-tcnative-boringssl-static:2.0.80.Final=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-tcnative-classes:2.0.80.Final=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-tcnative-boringssl-static:2.0.81.Final=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-tcnative-classes:2.0.81.Final=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-transport-native-unix-common:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-transport:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opencensus:opencensus-api:0.31.1=deploy_jar,runtimeClasspath,testRuntimeClasspath
+2 -2
View File
@@ -150,8 +150,8 @@ io.netty:netty-codec:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,t
io.netty:netty-common:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-handler:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-resolver:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-tcnative-boringssl-static:2.0.80.Final=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-tcnative-classes:2.0.80.Final=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-tcnative-boringssl-static:2.0.81.Final=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-tcnative-classes:2.0.81.Final=deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-transport-native-unix-common:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.netty:netty-transport:4.2.16.Final=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath
io.opencensus:opencensus-api:0.31.1=compileClasspath,deploy_jar,runtimeClasspath,testCompileClasspath,testRuntimeClasspath