mirror of
https://github.com/google/nomulus
synced 2026-07-23 16:43:05 +00:00
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:
Generated
-123
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user