1
0
mirror of https://github.com/google/nomulus synced 2026-01-17 11:13:03 +00:00

Compare commits

...

12 Commits

Author SHA1 Message Date
Pavlo Tkach
dfef733360 Incerase memory request for pubapi and frontend to 1Gi (#2743) 2025-04-11 16:17:43 +00:00
Pavlo Tkach
04a0659197 Disable console whois (#2741) 2025-04-11 15:32:34 +00:00
Pavlo Tkach
70010886b1 Increase hikari maximum pool size to 20 (#2742) 2025-04-10 20:51:51 +00:00
gbrodman
3cd50dc929 Only use GKE logs in ICANN reports (#2738)
We no longer need to union GKE+GAE logs since we've moved all production
traffic to GKE only.

For testing, I copied the affected *_test.sql files to Bigquery, removed
all the "-alpha" bits, and changed the dates to 20250301 and 20250331
and ran them to make sure they returned the expected data.
2025-04-09 17:12:02 +00:00
Pavlo Tkach
03872b508f Exclude prober endoint from sed command canary (#2739) 2025-04-07 21:13:13 +00:00
Pavlo Tkach
1096f201cd Add GKE readiness probe (#2735) 2025-04-04 21:33:43 +00:00
gbrodman
9dc3215624 Redirect an empty RDAP path to the /help response (#2722)
The behavior when someone hits the plain RDAP base URL isn't specified
by the spec. Currently we just return a plain 404 which isn't
particularly nice or helpful -- so it would probably be nicer to just
redirect to the /help response instead.

tested on alpha,
https://pubapi-dot-domain-registry-alpha.appspot.com/rdap redirects to https://pubapi-dot-domain-registry-alpha.appspot.com/rdap/help
2025-04-03 15:37:23 +00:00
Lai Jiang
af321fb65e Make frontend deployment auto scale (#2736)
Now that we have effective global sessions thanks to #2734, there is no
longer a need to keep the number of pods on the EPP service static.

We are also not vulnerable to random pod restarts. K8s never guarantees
perpetual pod lifetime anyway, and not having to be at its mercy is
certainly a relief.
2025-04-02 18:58:52 +00:00
Lai Jiang
c5132c04be Use pipe as extension URI separator (#2737)
It turns out period can be used in the URI, such as in
"urn:ietf:params:xml:ns:fee-0.12". I don't think pipe is used, at least
not according to EPP URI namespace naming convention.

Ideally we'd use serialization, but using the default serialization runs
the risk of it being platform/JDK dependent, so a new deployment might
not be able to deserialize existing cookies. A custom serializer that
guarantees stability would have been needed.
2025-04-02 13:21:13 +00:00
Lai Jiang
a64dc21f96 make the deploy task deploy to GKE (#2734)
Also always pulls the latest images from repos instead of relying on
local cases. This makes it so that a local docker build is always fresh.
2025-03-31 22:38:53 +00:00
Pavlo Tkach
0381533a35 Set grace period to 1s for immediate pods restart (#2733) 2025-03-31 19:15:13 +00:00
Lai Jiang
4999a72d96 Save session data directly in a cookie (#2732) 2025-03-31 16:21:50 +00:00
62 changed files with 858 additions and 437 deletions

View File

@@ -90,7 +90,6 @@ explodeWar.doLast {
appengineDeployAll.mustRunAfter ':console-webapp:deploy'
appengineDeployAll.finalizedBy ':deployCloudSchedulerAndQueue'
rootProject.deploy.dependsOn appengineDeployAll
rootProject.stage.dependsOn appengineStage
tasks['war'].dependsOn ':core:processResources'

View File

@@ -101,16 +101,9 @@ task checkFormatting(type: Exec) {
args 'run', 'prettify:check'
}
task deploy(type: Exec) {
workingDir "${consoleDir}/staged"
executable 'gcloud'
args 'app', 'deploy', "${projectParam}", '--quiet'
}
tasks.buildConsoleWebapp.dependsOn(tasks.npmInstallDeps)
tasks.runConsoleWebappUnitTests.dependsOn(tasks.npmInstallDeps)
tasks.applyFormatting.dependsOn(tasks.npmInstallDeps)
tasks.checkFormatting.dependsOn(tasks.npmInstallDeps)
tasks.build.dependsOn(tasks.checkFormatting)
tasks.build.dependsOn(tasks.runConsoleWebappUnitTests)
tasks.deploy.dependsOn(tasks.buildConsoleWebapp)

View File

@@ -24,8 +24,8 @@ import { ResourcesComponent } from './resources/resources.component';
import ContactComponent from './settings/contact/contact.component';
import SecurityComponent from './settings/security/security.component';
import { SettingsComponent } from './settings/settings.component';
import WhoisComponent from './settings/whois/whois.component';
import { SupportComponent } from './support/support.component';
import RdapComponent from './settings/rdap/rdap.component';
export interface RouteWithIcon extends Route {
iconName?: string;
@@ -83,9 +83,9 @@ export const routes: RouteWithIcon[] = [
title: 'Contacts',
},
{
path: WhoisComponent.PATH,
component: WhoisComponent,
title: 'WHOIS Info',
path: RdapComponent.PATH,
component: RdapComponent,
title: 'RDAP Info',
},
{
path: SecurityComponent.PATH,

View File

@@ -47,8 +47,6 @@ import EppPasswordEditComponent from './settings/security/eppPasswordEdit.compon
import SecurityComponent from './settings/security/security.component';
import SecurityEditComponent from './settings/security/securityEdit.component';
import { SettingsComponent } from './settings/settings.component';
import WhoisComponent from './settings/whois/whois.component';
import WhoisEditComponent from './settings/whois/whoisEdit.component';
import { NotificationsComponent } from './shared/components/notifications/notifications.component';
import { SelectedRegistrarWrapper } from './shared/components/selectedRegistrarWrapper/selectedRegistrarWrapper.component';
import { LocationBackDirective } from './shared/directives/locationBack.directive';
@@ -60,6 +58,8 @@ import { SnackBarModule } from './snackbar.module';
import { SupportComponent } from './support/support.component';
import { TldsComponent } from './tlds/tlds.component';
import { ForceFocusDirective } from './shared/directives/forceFocus.directive';
import RdapComponent from './settings/rdap/rdap.component';
import RdapEditComponent from './settings/rdap/rdapEdit.component';
@NgModule({
declarations: [SelectedRegistrarWrapper],
@@ -76,30 +76,30 @@ export class SelectedRegistrarModule {}
ContactDetailsComponent,
DomainListComponent,
EppPasswordEditComponent,
ForceFocusDirective,
HeaderComponent,
HomeComponent,
LocationBackDirective,
ForceFocusDirective,
UserLevelVisibility,
NavigationComponent,
NewRegistrarComponent,
NotificationsComponent,
RdapComponent,
RdapEditComponent,
ReasonDialogComponent,
RegistrarComponent,
RegistrarDetailsComponent,
RegistryLockComponent,
RegistrarSelectorComponent,
RegistryLockComponent,
RegistryLockVerifyComponent,
ResourcesComponent,
ResponseDialogComponent,
SecurityComponent,
SecurityEditComponent,
SettingsComponent,
SettingsContactComponent,
SupportComponent,
TldsComponent,
WhoisComponent,
WhoisEditComponent,
ReasonDialogComponent,
ResponseDialogComponent,
UserLevelVisibility,
],
bootstrap: [AppComponent],
imports: [
@@ -108,8 +108,8 @@ export class SelectedRegistrarModule {}
BrowserModule,
FormsModule,
MaterialModule,
SnackBarModule,
SelectedRegistrarModule,
SnackBarModule,
],
providers: [
BackendService,

View File

@@ -48,7 +48,6 @@ export default class NewRegistrarComponent {
this.newRegistrar = {
registrarId: '',
url: '',
whoisServer: '',
registrarName: '',
icannReferralEmail: '',
localizedAddress: {

View File

@@ -50,17 +50,16 @@ export interface SecuritySettings
ipAddressAllowList?: Array<IpAllowListItem>;
}
export interface WhoisRegistrarFields {
export interface RdapRegistrarFields {
ianaIdentifier?: number;
icannReferralEmail: string;
localizedAddress: Address;
registrarId: string;
url: string;
whoisServer: string;
}
export interface Registrar
extends WhoisRegistrarFields,
extends RdapRegistrarFields,
SecuritySettingsBackendModel {
allowedTlds?: string[];
billingAccountMap?: object;

View File

@@ -24,7 +24,7 @@ export type contactType =
| 'LEGAL'
| 'MARKETING'
| 'TECH'
| 'WHOIS';
| 'RDAP';
type contactTypesToUserFriendlyTypes = { [type in contactType]: string };
@@ -35,7 +35,7 @@ export const contactTypeToTextMap: contactTypesToUserFriendlyTypes = {
LEGAL: 'Legal contact',
MARKETING: 'Marketing contact',
TECH: 'Technical contact',
WHOIS: 'WHOIS-Inquiry contact',
RDAP: 'RDAP-Inquiry contact',
};
type UserFriendlyType = (typeof contactTypeToTextMap)[contactType];

View File

@@ -97,12 +97,12 @@
</section>
<section>
<h1>WHOIS Preferences</h1>
<h1>RDAP Preferences</h1>
<div>
<mat-checkbox
[(ngModel)]="contactService.contactInEdit.visibleInWhoisAsAdmin"
[ngModelOptions]="{ standalone: true }"
>Show in Registrar WHOIS record as admin contact</mat-checkbox
>Show in Registrar RDAP record as admin contact</mat-checkbox
>
</div>
@@ -110,7 +110,7 @@
<mat-checkbox
[(ngModel)]="contactService.contactInEdit.visibleInWhoisAsTech"
[ngModelOptions]="{ standalone: true }"
>Show in Registrar WHOIS record as technical contact</mat-checkbox
>Show in Registrar RDAP record as technical contact</mat-checkbox
>
</div>
@@ -118,8 +118,8 @@
<mat-checkbox
[(ngModel)]="contactService.contactInEdit.visibleInDomainWhoisAsAbuse"
[ngModelOptions]="{ standalone: true }"
>Show Phone and Email in Domain WHOIS Record as registrar abuse
contact (per CL&D requirements)</mat-checkbox
>Show Phone and Email in Domain RDAP Record as registrar abuse contact
(per CL&D requirements)</mat-checkbox
>
</div>
</section>
@@ -176,13 +176,13 @@
<mat-card-content>
<mat-list role="list">
<mat-list-item role="listitem">
<h2>WHOIS Preferences</h2>
<h2>RDAP Preferences</h2>
</mat-list-item>
@if(contactService.contactInEdit.visibleInWhoisAsAdmin) {
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-value"
>Show in Registrar WHOIS record as admin contact</span
>Show in Registrar RDAP record as admin contact</span
>
</mat-list-item>
} @if(contactService.contactInEdit.visibleInWhoisAsTech) {
@@ -192,14 +192,14 @@
*ngIf="contactService.contactInEdit.visibleInWhoisAsTech"
>
<span class="console-app__list-value"
>Show in Registrar WHOIS record as technical contact</span
>Show in Registrar RDAP record as technical contact</span
>
</mat-list-item>
} @if(contactService.contactInEdit.visibleInDomainWhoisAsAbuse) {
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-value"
>Show Phone and Email in Domain WHOIS Record as registrar abuse
>Show Phone and Email in Domain RDAP Record as registrar abuse
contact (per CL&D requirements)</span
>
</mat-list-item>

View File

@@ -1,18 +1,18 @@
@if(whoisService.editing) {
<app-whois-edit></app-whois-edit>
@if(rdapService.editing) {
<app-rdap-edit></app-rdap-edit>
} @else {
<div class="console-app__whois">
<div class="console-app__whois-controls">
<div class="console-app__rdap">
<div class="console-app__rdap-controls">
<span>
General registrar information for your WHOIS record. This information is
always visible in WHOIS.
General registrar information for your RDAP record. This information is
always visible in RDAP.
</span>
<div class="spacer"></div>
<button
mat-flat-button
color="primary"
aria-label="Edit WHOIS record"
(click)="whoisService.editing = true"
aria-label="Edit RDAP record"
(click)="rdapService.editing = true"
>
<mat-icon>edit</mat-icon>
Edit
@@ -61,45 +61,5 @@
</mat-list>
</mat-card-content>
</mat-card>
<mat-card appearance="outlined">
<mat-card-content>
<mat-list role="list">
<mat-list-item role="listitem">
<h2>Technical Info</h2>
</mat-list-item>
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-key">IANA Identifier</span>
<span class="console-app__list-value">{{
registrarService.registrar()?.ianaIdentifier
}}</span>
</mat-list-item>
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<div>
<span class="console-app__list-key">ICANN Referral Email</span>
<span class="console-app__list-value">{{
registrarService.registrar()?.icannReferralEmail
}}</span>
</div>
</mat-list-item>
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-key">WHOIS server</span>
<span class="console-app__list-value">{{
registrarService.registrar()?.whoisServer
}}</span>
</mat-list-item>
<mat-divider></mat-divider>
<mat-list-item role="listitem">
<span class="console-app__list-key">Referral URL</span>
<span class="console-app__list-value">{{
registrarService.registrar()?.url
}}</span>
</mat-list-item>
</mat-list>
</mat-card-content>
</mat-card>
</div>
}

View File

@@ -1,4 +1,4 @@
.console-app__whois {
.console-app__rdap {
max-width: 616px;
&-controls {

View File

@@ -20,15 +20,15 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MaterialModule } from 'src/app/material.module';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import { BackendService } from 'src/app/shared/services/backend.service';
import WhoisComponent from './whois.component';
import RdapComponent from './rdap.component';
describe('WhoisComponent', () => {
let component: WhoisComponent;
let fixture: ComponentFixture<WhoisComponent>;
describe('RdapComponent', () => {
let component: RdapComponent;
let fixture: ComponentFixture<RdapComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [WhoisComponent],
declarations: [RdapComponent],
imports: [MaterialModule, BrowserAnimationsModule],
providers: [
BackendService,
@@ -45,7 +45,7 @@ describe('WhoisComponent', () => {
],
}).compileComponents();
fixture = TestBed.createComponent(WhoisComponent);
fixture = TestBed.createComponent(RdapComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@@ -14,17 +14,16 @@
import { Component, computed } from '@angular/core';
import { RegistrarService } from 'src/app/registrar/registrar.service';
import { WhoisService } from './whois.service';
import { RdapService } from './rdap.service';
@Component({
selector: 'app-whois',
templateUrl: './whois.component.html',
styleUrls: ['./whois.component.scss'],
selector: 'app-rdap',
templateUrl: './rdap.component.html',
styleUrls: ['./rdap.component.scss'],
standalone: false,
})
export default class WhoisComponent {
public static PATH = 'whois';
export default class RdapComponent {
public static PATH = 'rdap';
formattedAddress = computed(() => {
let result = '';
const registrar = this.registrarService.registrar();
@@ -47,7 +46,7 @@ export default class WhoisComponent {
});
constructor(
public whoisService: WhoisService,
public rdapService: RdapService,
public registrarService: RegistrarService
) {}
}

View File

@@ -16,14 +16,14 @@ import { Injectable } from '@angular/core';
import { switchMap } from 'rxjs';
import {
RegistrarService,
WhoisRegistrarFields,
RdapRegistrarFields,
} from 'src/app/registrar/registrar.service';
import { BackendService } from 'src/app/shared/services/backend.service';
@Injectable({
providedIn: 'root',
})
export class WhoisService {
export class RdapService {
editing: boolean = false;
constructor(
@@ -31,8 +31,8 @@ export class WhoisService {
private registrarService: RegistrarService
) {}
saveChanges(newWhoisRegistrarFields: WhoisRegistrarFields) {
return this.backend.postWhoisRegistrarFields(newWhoisRegistrarFields).pipe(
saveChanges(newRdapRegistrarFields: RdapRegistrarFields) {
return this.backend.postRdapRegistrarFields(newRdapRegistrarFields).pipe(
switchMap(() => {
return this.registrarService.loadRegistrars();
})

View File

@@ -1,27 +1,27 @@
<div
class="console-app__whois-edit"
class="console-app__rdap-edit"
*ngIf="registrarInEdit"
cdkTrapFocus
[cdkTrapFocusAutoCapture]="true"
>
<button
mat-icon-button
class="console-app__whois-edit-back"
aria-label="Back to whois view"
(click)="whoisService.editing = false"
class="console-app__rdap-edit-back"
aria-label="Back to rdap view"
(click)="rdapService.editing = false"
>
<mat-icon>arrow_back</mat-icon>
</button>
<div class="console-app__whois-edit-controls">
<div class="console-app__rdap-edit-controls">
<span>
General registrar information for your WHOIS record. This information is
always visible in WHOIS.
General registrar information for your RDAP record. This information is
always visible in RDAP.
</span>
<div class="spacer"></div>
</div>
<div class="console-app__whois-edit">
<div class="console-app__rdap-edit">
<h1>Personal info</h1>
<form (ngSubmit)="save($event)">
@@ -115,45 +115,11 @@
/>
</mat-form-field>
<h1>Technical info</h1>
<mat-form-field appearance="outline">
<mat-label>WHOIS server: </mat-label>
<input
matInput
type="text"
[(ngModel)]="registrarInEdit.whoisServer"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Referral URL: </mat-label>
<input
matInput
type="text"
[(ngModel)]="registrarInEdit.url"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
@if((userDataService.userData()?.globalRole || 'NONE') !== "NONE") {
<mat-form-field appearance="outline">
<mat-label>ICANN Referral Email: </mat-label>
<input
matInput
type="text"
[(ngModel)]="registrarInEdit.icannReferralEmail"
[ngModelOptions]="{ standalone: true }"
/>
</mat-form-field>
}
<button
mat-flat-button
color="primary"
type="submit"
aria-label="Save WHOIS settings"
aria-label="Save RDAO settings"
>
Save
</button>

View File

@@ -1,4 +1,4 @@
.console-app__whois-edit {
.console-app__rdap-edit {
max-width: 616px;
&-controls {

View File

@@ -20,20 +20,20 @@ import {
RegistrarService,
} from 'src/app/registrar/registrar.service';
import { UserDataService } from 'src/app/shared/services/userData.service';
import { WhoisService } from './whois.service';
import { RdapService } from './rdap.service';
@Component({
selector: 'app-whois-edit',
templateUrl: './whoisEdit.component.html',
styleUrls: ['./whoisEdit.component.scss'],
selector: 'app-rdap-edit',
templateUrl: './rdapEdit.component.html',
styleUrls: ['./rdapEdit.component.scss'],
standalone: false,
})
export default class WhoisEditComponent {
export default class RdapEditComponent {
registrarInEdit: Registrar | undefined;
constructor(
public userDataService: UserDataService,
public whoisService: WhoisService,
public rdapService: RdapService,
public registrarService: RegistrarService,
private _snackBar: MatSnackBar
) {
@@ -49,9 +49,9 @@ export default class WhoisEditComponent {
e.preventDefault();
if (!this.registrarInEdit) return;
this.whoisService.saveChanges(this.registrarInEdit).subscribe({
this.rdapService.saveChanges(this.registrarInEdit).subscribe({
complete: () => {
this.whoisService.editing = false;
this.rdapService.editing = false;
},
error: (err: HttpErrorResponse) => {
this._snackBar.open(err.error);

View File

@@ -19,13 +19,13 @@
>
<a
mat-tab-link
routerLink="whois"
routerLink="rdap"
routerLinkActive
queryParamsHandling="merge"
#rla2="routerLinkActive"
[active]="rla2.isActive"
aria-label="Access whois settings"
>WHOIS Info</a
aria-label="Access rdap settings"
>RDAP Info</a
>
<a
mat-tab-link

View File

@@ -25,7 +25,7 @@ import { User } from 'src/app/users/users.service';
import {
Registrar,
SecuritySettingsBackendModel,
WhoisRegistrarFields,
RdapRegistrarFields,
} from '../../registrar/registrar.service';
import { Contact } from '../../settings/contact/contact.service';
import { EppPasswordBackendModel } from '../../settings/security/security.service';
@@ -209,12 +209,12 @@ export class BackendService {
.pipe(catchError((err) => this.errorCatcher<UserData>(err)));
}
postWhoisRegistrarFields(
whoisRegistrarFields: WhoisRegistrarFields
): Observable<WhoisRegistrarFields> {
return this.http.post<WhoisRegistrarFields>(
'/console-api/settings/whois-fields',
whoisRegistrarFields
postRdapRegistrarFields(
rdapRegistrarFields: RdapRegistrarFields
): Observable<RdapRegistrarFields> {
return this.http.post<RdapRegistrarFields>(
'/console-api/settings/rdap-fields',
rdapRegistrarFields
);
}

View File

@@ -243,7 +243,7 @@ hibernate:
# that BEAM pipelines are not subject to the maximumPoolSize value defined
# here. See PersistenceModule.java for more information.
hikariMinimumIdle: 1
hikariMaximumPoolSize: 10
hikariMaximumPoolSize: 20
hikariIdleTimeout: 300000
# The batch size is basically the number of insertions / updates in a single
# transaction that will be batched together into one INSERT/UPDATE statement.

View File

@@ -0,0 +1,155 @@
// Copyright 2025 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.flows;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.BaseEncoding;
import google.registry.request.Response;
import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A metadata class that saves the data directly in cookies.
*
* <p>Unlike {@link HttpSessionMetadata}, this class does not rely on a session manager to translate
* an opaque session cookie into the metadata. This means that the locality of the session manager
* is irrelevant and as long as the client (the proxy) respects the {@code Set-Cookie} headers and
* sets the respective cookies in subsequent requests in a session, the metadata will be available
* to all servers, not just the one that created the session.
*
* <p>The string representation of the metadata is saved in Base64 URL-safe format in a cookie named
* {@code SESSION_INFO}.
*/
public class CookieSessionMetadata extends SessionMetadata {
protected static final String COOKIE_NAME = "SESSION_INFO";
protected static final String REGISTRAR_ID = "clientId";
protected static final String SERVICE_EXTENSIONS = "serviceExtensionUris";
protected static final String FAILED_LOGIN_ATTEMPTS = "failedLoginAttempts";
private static final Pattern COOKIE_PATTERN = Pattern.compile("SESSION_INFO=([^;\\s]+)?");
private static final Pattern REGISTRAR_ID_PATTERN = Pattern.compile("clientId=([^,\\s]+)?");
private static final Pattern SERVICE_EXTENSIONS_PATTERN =
Pattern.compile("serviceExtensionUris=([^,\\s}]+)?");
private static final Pattern FAILED_LOGIN_ATTEMPTS_PATTERN =
Pattern.compile("failedLoginAttempts=([^,\\s]+)?");
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final Map<String, String> data = new HashMap<>();
public CookieSessionMetadata(HttpServletRequest request) {
Optional.ofNullable(request.getHeader("Cookie"))
.ifPresent(
cookie -> {
Matcher matcher = COOKIE_PATTERN.matcher(cookie);
if (matcher.find()) {
String sessionInfo = decode(matcher.group(1));
logger.atInfo().log("SESSION INFO: %s", sessionInfo);
matcher = REGISTRAR_ID_PATTERN.matcher(sessionInfo);
if (matcher.find()) {
String registrarId = matcher.group(1);
if (!registrarId.equals("null")) {
data.put(REGISTRAR_ID, registrarId);
}
}
matcher = SERVICE_EXTENSIONS_PATTERN.matcher(sessionInfo);
if (matcher.find()) {
String serviceExtensions = matcher.group(1);
if (serviceExtensions != null) {
data.put(SERVICE_EXTENSIONS, serviceExtensions);
}
}
matcher = FAILED_LOGIN_ATTEMPTS_PATTERN.matcher(sessionInfo);
if (matcher.find()) {
String failedLoginAttempts = matcher.group(1);
data.put(FAILED_LOGIN_ATTEMPTS, failedLoginAttempts);
}
}
});
}
@Override
public void invalidate() {
data.clear();
}
@Override
public String getRegistrarId() {
return data.getOrDefault(REGISTRAR_ID, null);
}
@Override
public Set<String> getServiceExtensionUris() {
return Optional.ofNullable(data.getOrDefault(SERVICE_EXTENSIONS, null))
.map(s -> Splitter.on(URI_SEPARATOR).splitToList(s))
.map(ImmutableSet::copyOf)
.orElse(ImmutableSet.of());
}
@Override
public int getFailedLoginAttempts() {
return Optional.ofNullable(data.getOrDefault(FAILED_LOGIN_ATTEMPTS, null))
.map(Integer::parseInt)
.orElse(0);
}
@Override
public void setRegistrarId(String registrarId) {
data.put(REGISTRAR_ID, registrarId);
}
@Override
public void setServiceExtensionUris(Set<String> serviceExtensionUris) {
if (serviceExtensionUris == null || serviceExtensionUris.isEmpty()) {
data.remove(SERVICE_EXTENSIONS);
} else {
data.put(SERVICE_EXTENSIONS, Joiner.on(URI_SEPARATOR).join(serviceExtensionUris));
}
}
@Override
public void incrementFailedLoginAttempts() {
data.put(FAILED_LOGIN_ATTEMPTS, String.valueOf(getFailedLoginAttempts() + 1));
}
@Override
public void resetFailedLoginAttempts() {
data.remove(FAILED_LOGIN_ATTEMPTS);
}
@Override
public void save(Response response) {
String value = encode(toString());
response.setHeader("Set-Cookie", COOKIE_NAME + "=" + value);
}
protected static String encode(String plainText) {
return BaseEncoding.base64Url().encode(plainText.getBytes(US_ASCII));
}
protected static String decode(String cipherText) {
return new String(BaseEncoding.base64Url().decode(cipherText), US_ASCII);
}
}

View File

@@ -78,6 +78,8 @@ public class EppRequestHandler {
} catch (Exception e) {
logger.atWarning().withCause(e).log("handleEppCommand general exception.");
response.setStatus(SC_BAD_REQUEST);
} finally {
sessionMetadata.save(response);
}
}
}

View File

@@ -20,7 +20,7 @@ import google.registry.request.Action.Method;
import google.registry.request.Payload;
import google.registry.request.auth.Auth;
import jakarta.inject.Inject;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpServletRequest;
/**
* Establishes a transport for EPP+TLS over HTTP. All commands and responses are EPP XML according
@@ -35,18 +35,18 @@ public class EppTlsAction implements Runnable {
@Inject @Payload byte[] inputXmlBytes;
@Inject TlsCredentials tlsCredentials;
@Inject HttpSession session;
@Inject HttpServletRequest request;
@Inject EppRequestHandler eppRequestHandler;
@Inject EppTlsAction() {}
@Override
public void run() {
eppRequestHandler.executeEpp(
new HttpSessionMetadata(session),
new CookieSessionMetadata(request),
tlsCredentials,
EppRequestSource.TLS,
false, // This endpoint is never a dry run.
false, // This endpoint is never a superuser.
false, // This endpoint is never a dry run.
false, // This endpoint is never a superuser.
inputXmlBytes);
}
}

View File

@@ -14,16 +14,14 @@
package google.registry.flows;
import static com.google.common.base.MoreObjects.toStringHelper;
import static google.registry.util.CollectionUtils.nullToEmpty;
import com.google.common.base.Joiner;
import jakarta.servlet.http.HttpSession;
import java.util.Optional;
import java.util.Set;
/** A metadata class that is a wrapper around {@link HttpSession}. */
public class HttpSessionMetadata implements SessionMetadata {
public class HttpSessionMetadata extends SessionMetadata {
private static final String REGISTRAR_ID = "REGISTRAR_ID";
private static final String SERVICE_EXTENSIONS = "SERVICE_EXTENSIONS";
@@ -75,13 +73,4 @@ public class HttpSessionMetadata implements SessionMetadata {
public void resetFailedLoginAttempts() {
session.removeAttribute(FAILED_LOGIN_ATTEMPTS);
}
@Override
public String toString() {
return toStringHelper(getClass())
.add("clientId", getRegistrarId())
.add("failedLoginAttempts", getFailedLoginAttempts())
.add("serviceExtensionUris", Joiner.on('.').join(nullToEmpty(getServiceExtensionUris())))
.toString();
}
}

View File

@@ -14,29 +14,49 @@
package google.registry.flows;
import static com.google.common.base.MoreObjects.toStringHelper;
import static google.registry.util.CollectionUtils.nullToEmpty;
import com.google.common.base.Joiner;
import google.registry.request.Response;
import java.util.Set;
/** Object to allow setting and retrieving session information in flows. */
public interface SessionMetadata {
public abstract class SessionMetadata {
protected static final char URI_SEPARATOR = '|';
/**
* Invalidates the session. A new instance must be created after this for future sessions.
* Attempts to invoke methods of this class after this method has been called will throw
* {@code IllegalStateException}.
* Attempts to invoke methods of this class after this method has been called will throw {@code
* IllegalStateException}.
*/
void invalidate();
public abstract void invalidate();
String getRegistrarId();
public abstract String getRegistrarId();
Set<String> getServiceExtensionUris();
public abstract Set<String> getServiceExtensionUris();
int getFailedLoginAttempts();
public abstract int getFailedLoginAttempts();
void setRegistrarId(String registrarId);
public abstract void setRegistrarId(String registrarId);
void setServiceExtensionUris(Set<String> serviceExtensionUris);
public abstract void setServiceExtensionUris(Set<String> serviceExtensionUris);
void incrementFailedLoginAttempts();
public abstract void incrementFailedLoginAttempts();
void resetFailedLoginAttempts();
public abstract void resetFailedLoginAttempts();
@Override
public String toString() {
return toStringHelper(getClass())
.add("clientId", getRegistrarId())
.add("failedLoginAttempts", getFailedLoginAttempts())
.add(
"serviceExtensionUris",
Joiner.on(URI_SEPARATOR).join(nullToEmpty(getServiceExtensionUris())))
.toString();
}
public void save(Response response) {}
}

View File

@@ -14,16 +14,13 @@
package google.registry.flows;
import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.util.CollectionUtils.nullToEmpty;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import java.util.Set;
/** A read-only {@link SessionMetadata} that doesn't support login/logout. */
public class StatelessRequestSessionMetadata implements SessionMetadata {
public class StatelessRequestSessionMetadata extends SessionMetadata {
private final String registrarId;
private final ImmutableSet<String> serviceExtensionUris;
@@ -74,13 +71,6 @@ public class StatelessRequestSessionMetadata implements SessionMetadata {
throw new UnsupportedOperationException();
}
@Override
public String toString() {
return toStringHelper(getClass())
.add("clientId", getRegistrarId())
.add("failedLoginAttempts", getFailedLoginAttempts())
.add("serviceExtensionUris", Joiner.on('.').join(nullToEmpty(getServiceExtensionUris())))
.toString();
}
}

View File

@@ -0,0 +1,96 @@
// Copyright 2025 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 static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.flogger.FluentLogger;
import google.registry.request.Action;
import google.registry.request.Action.GaeService;
import google.registry.request.Action.GkeService;
import google.registry.request.auth.Auth;
import jakarta.inject.Inject;
import jakarta.servlet.http.HttpServletResponse;
public class ReadinessProbeAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final HttpServletResponse rsp;
public ReadinessProbeAction(HttpServletResponse rsp) {
this.rsp = rsp;
}
/**
* Executes the readiness check.
*
* <p>Performs a simple database query and sets the HTTP response status to OK (200) upon
* successful completion. Throws a runtime exception if the database query fails.
*/
@Override
public final void run() {
logger.atInfo().log("Performing readiness check database query...");
try {
tm().transact(() -> tm().query("SELECT version()", Void.class));
rsp.setStatus(SC_OK);
logger.atInfo().log("Readiness check successful.");
} catch (Exception e) {
logger.atWarning().withCause(e).log("Readiness check failed:");
throw new RuntimeException("Readiness check failed during database query", e);
}
}
@Action(
service = GaeService.DEFAULT,
gkeService = GkeService.CONSOLE,
path = ReadinessProbeConsoleAction.PATH,
auth = Auth.AUTH_PUBLIC)
public static class ReadinessProbeConsoleAction extends ReadinessProbeAction {
public static final String PATH = "/ready/console";
@Inject
public ReadinessProbeConsoleAction(HttpServletResponse rsp) {
super(rsp);
}
}
@Action(
service = GaeService.PUBAPI,
gkeService = GkeService.PUBAPI,
path = ReadinessProbeActionPubApi.PATH,
auth = Auth.AUTH_PUBLIC)
public static class ReadinessProbeActionPubApi extends ReadinessProbeAction {
public static final String PATH = "/ready/pubapi";
@Inject
public ReadinessProbeActionPubApi(HttpServletResponse rsp) {
super(rsp);
}
}
@Action(
service = GaeService.DEFAULT,
gkeService = GkeService.FRONTEND,
path = ReadinessProbeActionFrontend.PATH,
auth = Auth.AUTH_PUBLIC)
public static final class ReadinessProbeActionFrontend extends ReadinessProbeAction {
public static final String PATH = "/ready/frontend";
@Inject
public ReadinessProbeActionFrontend(HttpServletResponse rsp) {
super(rsp);
}
}
}

View File

@@ -58,10 +58,14 @@ import google.registry.flows.TlsCredentials.EppTlsModule;
import google.registry.flows.custom.CustomLogicModule;
import google.registry.loadtest.LoadTestAction;
import google.registry.loadtest.LoadTestModule;
import google.registry.module.ReadinessProbeAction.ReadinessProbeActionFrontend;
import google.registry.module.ReadinessProbeAction.ReadinessProbeActionPubApi;
import google.registry.module.ReadinessProbeAction.ReadinessProbeConsoleAction;
import google.registry.monitoring.whitebox.WhiteboxModule;
import google.registry.rdap.RdapAutnumAction;
import google.registry.rdap.RdapDomainAction;
import google.registry.rdap.RdapDomainSearchAction;
import google.registry.rdap.RdapEmptyAction;
import google.registry.rdap.RdapEntityAction;
import google.registry.rdap.RdapEntitySearchAction;
import google.registry.rdap.RdapHelpAction;
@@ -123,8 +127,8 @@ import google.registry.ui.server.console.ConsoleUsersAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.domains.ConsoleBulkDomainAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.RdapRegistrarFieldsAction;
import google.registry.ui.server.console.settings.SecurityAction;
import google.registry.ui.server.console.settings.WhoisRegistrarFieldsAction;
import google.registry.whois.WhoisAction;
import google.registry.whois.WhoisHttpAction;
import google.registry.whois.WhoisModule;
@@ -255,12 +259,20 @@ interface RequestComponent {
PublishSpec11ReportAction publishSpec11ReportAction();
ReadinessProbeConsoleAction readinessProbeConsoleAction();
ReadinessProbeActionPubApi readinessProbeActionPubApi();
ReadinessProbeActionFrontend readinessProbeActionFrontend();
RdapAutnumAction rdapAutnumAction();
RdapDomainAction rdapDomainAction();
RdapDomainSearchAction rdapDomainSearchAction();
RdapEmptyAction rdapEmptyAction();
RdapEntityAction rdapEntityAction();
RdapEntitySearchAction rdapEntitySearchAction();
@@ -325,7 +337,7 @@ interface RequestComponent {
WhoisHttpAction whoisHttpAction();
WhoisRegistrarFieldsAction whoisRegistrarFieldsAction();
RdapRegistrarFieldsAction rdapRegistrarFieldsAction();
WipeOutContactHistoryPiiAction wipeOutContactHistoryPiiAction();

View File

@@ -21,6 +21,8 @@ import google.registry.dns.DnsModule;
import google.registry.flows.EppTlsAction;
import google.registry.flows.FlowComponent;
import google.registry.flows.TlsCredentials.EppTlsModule;
import google.registry.module.ReadinessProbeAction.ReadinessProbeActionFrontend;
import google.registry.module.ReadinessProbeAction.ReadinessProbeConsoleAction;
import google.registry.monitoring.whitebox.WhiteboxModule;
import google.registry.request.RequestComponentBuilder;
import google.registry.request.RequestModule;
@@ -39,8 +41,8 @@ import google.registry.ui.server.console.ConsoleUsersAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.domains.ConsoleBulkDomainAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.RdapRegistrarFieldsAction;
import google.registry.ui.server.console.settings.SecurityAction;
import google.registry.ui.server.console.settings.WhoisRegistrarFieldsAction;
/** Dagger component with per-request lifetime for "default" App Engine module. */
@RequestScope
@@ -82,11 +84,15 @@ public interface FrontendRequestComponent {
FlowComponent.Builder flowComponentBuilder();
ReadinessProbeActionFrontend readinessProbeActionFrontend();
ReadinessProbeConsoleAction readinessProbeConsoleAction();
RegistrarsAction registrarsAction();
SecurityAction securityAction();
WhoisRegistrarFieldsAction whoisRegistrarFieldsAction();
RdapRegistrarFieldsAction rdapRegistrarFieldsAction();
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder<FrontendRequestComponent> {

View File

@@ -20,10 +20,12 @@ import google.registry.dns.DnsModule;
import google.registry.flows.CheckApiAction;
import google.registry.flows.CheckApiAction.CheckApiModule;
import google.registry.flows.TlsCredentials.EppTlsModule;
import google.registry.module.ReadinessProbeAction.ReadinessProbeActionPubApi;
import google.registry.monitoring.whitebox.WhiteboxModule;
import google.registry.rdap.RdapAutnumAction;
import google.registry.rdap.RdapDomainAction;
import google.registry.rdap.RdapDomainSearchAction;
import google.registry.rdap.RdapEmptyAction;
import google.registry.rdap.RdapEntityAction;
import google.registry.rdap.RdapEntitySearchAction;
import google.registry.rdap.RdapHelpAction;
@@ -55,12 +57,16 @@ public interface PubApiRequestComponent {
RdapAutnumAction rdapAutnumAction();
RdapDomainAction rdapDomainAction();
RdapDomainSearchAction rdapDomainSearchAction();
RdapEmptyAction rdapEmptyAction();
RdapEntityAction rdapEntityAction();
RdapEntitySearchAction rdapEntitySearchAction();
RdapHelpAction rdapHelpAction();
RdapIpAction rdapDefaultAction();
RdapNameserverAction rdapNameserverAction();
RdapNameserverSearchAction rdapNameserverSearchAction();
ReadinessProbeActionPubApi readinessProbeActionPubApi();
WhoisHttpAction whoisHttpAction();
WhoisAction whoisAction();

View File

@@ -0,0 +1,54 @@
// Copyright 2025 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.rdap;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import google.registry.request.Action;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import jakarta.inject.Inject;
import java.io.IOException;
/**
* RDAP action that serves the empty string, redirecting to the help page.
*
* <p>This isn't technically required, but if someone requests the base url it seems nice to give
* them the help response.
*/
@Action(
service = Action.GaeService.PUBAPI,
path = "/rdap/",
method = {GET, HEAD},
auth = Auth.AUTH_PUBLIC)
public class RdapEmptyAction implements Runnable {
private final Response response;
@Inject
public RdapEmptyAction(Response response) {
this.response = response;
}
@Override
public void run() {
try {
response.sendRedirect(RdapHelpAction.PATH);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -31,12 +31,14 @@ import java.util.Optional;
/** RDAP (new WHOIS) action for help requests. */
@Action(
service = GaeService.PUBAPI,
path = "/rdap/help",
path = RdapHelpAction.PATH,
method = {GET, HEAD},
isPrefix = true,
auth = Auth.AUTH_PUBLIC)
public class RdapHelpAction extends RdapActionBase {
public static final String PATH = "/rdap/help";
/** The help path for the RDAP terms of service. */
public static final String TOS_PATH = "/tos";

View File

@@ -85,9 +85,6 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
String monthlyLogsQuery =
SqlTemplate.create(getQueryFromFile(MONTHLY_LOGS + ".sql"))
.put("PROJECT_ID", projectId)
.put("APPENGINE_LOGS_DATA_SET", "appengine_logs")
.put("GKE_LOGS_DATA_SET", "gke_logs")
.put("REQUEST_TABLE", "appengine_googleapis_com_request_log_")
.put("FIRST_DAY_OF_MONTH", logTableFormatter.print(firstDayOfMonth))
.put("LAST_DAY_OF_MONTH", logTableFormatter.print(lastDayOfMonth))
.build();
@@ -96,9 +93,6 @@ public final class ActivityReportingQueryBuilder implements QueryBuilder {
String eppQuery =
SqlTemplate.create(getQueryFromFile(EPP_METRICS + ".sql"))
.put("PROJECT_ID", projectId)
.put("APPENGINE_LOGS_DATA_SET", "appengine_logs")
.put("GKE_LOGS_DATA_SET", "gke_logs")
.put("APP_LOGS_TABLE", "_var_log_app_")
.put("FIRST_DAY_OF_MONTH", logTableFormatter.print(firstDayOfMonth))
.put("LAST_DAY_OF_MONTH", logTableFormatter.print(lastDayOfMonth))
.build();

View File

@@ -112,9 +112,6 @@ public final class TransactionsReportingQueryBuilder implements QueryBuilder {
String attemptedAddsQuery =
SqlTemplate.create(getQueryFromFile(ATTEMPTED_ADDS + ".sql"))
.put("PROJECT_ID", projectId)
.put("APPENGINE_LOGS_DATA_SET", "appengine_logs")
.put("GKE_LOGS_DATA_SET", "gke_logs")
.put("APP_LOGS_TABLE", "_var_log_app_")
.put("FIRST_DAY_OF_MONTH", logTableFormatter.print(earliestReportTime))
.put("LAST_DAY_OF_MONTH", logTableFormatter.print(latestReportTime))
.build();

View File

@@ -143,8 +143,11 @@ public class RequestHandler<C> {
GkeService service = Action.ServiceGetter.get(route.get().action());
String expectedDomain = RegistryConfig.getServiceUrl(service).getHost();
String actualDomain = req.getServerName();
// If the hostname is "localhost", it must have come from the sidecar proxy.
if (!Objects.equals("localhost", actualDomain)
// If the request doesn't come from GKE readiness prober
String maybeUserAgent = Optional.ofNullable(req.getHeader("User-Agent")).orElse("");
if (!maybeUserAgent.startsWith("kube-probe")
// If the hostname is "localhost", it must have come from the sidecar proxy.
&& !Objects.equals("localhost", actualDomain)
&& !Objects.equals(actualDomain, expectedDomain)) {
logger.atWarning().log(
"Actual domain %s does not match expected domain %s", actualDomain, expectedDomain);

View File

@@ -17,7 +17,6 @@ package google.registry.ui.server.console.settings;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import static jakarta.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static jakarta.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
@@ -36,7 +35,6 @@ import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAcce
import google.registry.ui.server.console.ConsoleApiAction;
import google.registry.ui.server.console.ConsoleApiParams;
import jakarta.inject.Inject;
import java.util.Objects;
import java.util.Optional;
/**
@@ -48,17 +46,17 @@ import java.util.Optional;
@Action(
service = GaeService.DEFAULT,
gkeService = GkeService.CONSOLE,
path = WhoisRegistrarFieldsAction.PATH,
path = RdapRegistrarFieldsAction.PATH,
method = {POST},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class WhoisRegistrarFieldsAction extends ConsoleApiAction {
public class RdapRegistrarFieldsAction extends ConsoleApiAction {
static final String PATH = "/console-api/settings/whois-fields";
static final String PATH = "/console-api/settings/rdap-fields";
private final AuthenticatedRegistrarAccessor registrarAccessor;
private final Optional<Registrar> registrar;
@Inject
public WhoisRegistrarFieldsAction(
public RdapRegistrarFieldsAction(
ConsoleApiParams consoleApiParams,
AuthenticatedRegistrarAccessor registrarAccessor,
@Parameter("registrar") Optional<Registrar> registrar) {
@@ -72,10 +70,10 @@ public class WhoisRegistrarFieldsAction extends ConsoleApiAction {
checkArgument(registrar.isPresent(), "'registrar' parameter is not present");
checkPermission(
user, registrar.get().getRegistrarId(), ConsolePermission.EDIT_REGISTRAR_DETAILS);
tm().transact(() -> loadAndModifyRegistrar(registrar.get(), user));
tm().transact(() -> loadAndModifyRegistrar(registrar.get()));
}
private void loadAndModifyRegistrar(Registrar providedRegistrar, User user) {
private void loadAndModifyRegistrar(Registrar providedRegistrar) {
Registrar savedRegistrar;
try {
// reload to make sure the object has all the correct fields
@@ -85,24 +83,12 @@ public class WhoisRegistrarFieldsAction extends ConsoleApiAction {
return;
}
// icannReferralEmail can't be updated by partners, only by global users with
// EDIT_REGISTRAR_DETAILS permission
if (!Objects.equals(
savedRegistrar.getIcannReferralEmail(), providedRegistrar.getIcannReferralEmail())
&& !user.getUserRoles().hasGlobalPermission(ConsolePermission.EDIT_REGISTRAR_DETAILS)) {
setFailedResponse(
"Icann Referral Email update is not permitted for this user", SC_BAD_REQUEST);
}
Registrar newRegistrar =
savedRegistrar
.asBuilder()
.setWhoisServer(providedRegistrar.getWhoisServer())
.setUrl(providedRegistrar.getUrl())
.setLocalizedAddress(providedRegistrar.getLocalizedAddress())
.setPhoneNumber(providedRegistrar.getPhoneNumber())
.setFaxNumber(providedRegistrar.getFaxNumber())
.setIcannReferralEmail(providedRegistrar.getIcannReferralEmail())
.setEmailAddress(providedRegistrar.getEmailAddress())
.build();
tm().put(newRegistrar);

View File

@@ -16,7 +16,7 @@
-- Determine the number of attempted adds each registrar made.
-- Since the specification requests all 'attempted' adds, we regex the
-- monthly App Engine logs, searching for all create commands and associating
-- monthly GKE logs, searching for all create commands and associating
-- them with their corresponding registrars.
-- Example log generated by FlowReporter in App Engine and GKE logs:
@@ -27,7 +27,7 @@
-- ,"targetId":"","targetIds":[],"tld":"",
-- "tlds":[],"icannActivityReportField":""}
-- This outer select just converts the registrar's clientId to their name.
-- This outer select just converts the registrar's ID to their name.
SELECT
tld,
registrar_table.registrar_name AS registrar_name,
@@ -38,34 +38,20 @@ FROM (
JSON_EXTRACT_SCALAR(json, '$.tld') AS tld,
JSON_EXTRACT_SCALAR(json, '$.clientId') AS clientId,
COUNT(json) AS count
FROM ((
-- Extract JSON metadata package from monthly logs
FROM (
-- Extract JSON metadata package from monthly logs
SELECT
REGEXP_EXTRACT(textPayload, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$') AS json
REGEXP_EXTRACT(jsonPayload.message, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$') AS json
FROM
`%PROJECT_ID%.%APPENGINE_LOGS_DATA_SET%.%APP_LOGS_TABLE%*`
`%PROJECT_ID%.gke_logs.stderr_*`
WHERE
_TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%'
AND '%LAST_DAY_OF_MONTH%'
AND STARTS_WITH(textPayload, "FLOW-LOG-SIGNATURE-METADATA")
AND STARTS_WITH(jsonPayload.message, "FLOW-LOG-SIGNATURE-METADATA")
-- Look for domain creates
AND REGEXP_CONTAINS(textPayload, r'"commandType":"create","resourceType":"domain"')
AND REGEXP_CONTAINS(jsonPayload.message, r'"commandType":"create","resourceType":"domain"')
-- Filter prober data
AND NOT REGEXP_CONTAINS(textPayload, r'"prober-[a-z]{2}-((any)|(canary))"'))
UNION ALL (
-- Extract JSON metadata package from monthly logs
SELECT
REGEXP_EXTRACT(jsonPayload.message, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$') AS json
FROM
`%PROJECT_ID%.%GKE_LOGS_DATA_SET%.stderr_*`
WHERE
_TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%'
AND '%LAST_DAY_OF_MONTH%'
AND STARTS_WITH(jsonPayload.message, "FLOW-LOG-SIGNATURE-METADATA")
-- Look for domain creates
AND REGEXP_CONTAINS(jsonPayload.message, r'"commandType":"create","resourceType":"domain"')
-- Filter prober data
AND NOT REGEXP_CONTAINS(jsonPayload.message, r'"prober-[a-z]{2}-((any)|(canary))"')))
AND NOT REGEXP_CONTAINS(jsonPayload.message, r'"prober-[a-z]{2}-((any)|(canary))"'))
GROUP BY
tld,
clientId ) AS logs_table

View File

@@ -15,7 +15,7 @@
-- Query FlowReporter JSON log messages and calculate SRS metrics.
-- We use ugly regex's over the monthly appengine logs to determine how many
-- We use ugly regexes over the monthly GKE logs to determine how many
-- EPP requests we received for each command. For example:
-- {"commandType":"check"...,"targetIds":["ais.a.how"],
-- "tld":"","tlds":["a.how"],"icannActivityReportField":"srs-dom-check"}
@@ -35,30 +35,15 @@ FROM (
JSON_EXTRACT_SCALAR(json,
'$.icannActivityReportField') AS activityReportField
FROM (
-- For reasons that I don't understand, if I directly select the three columns
-- from the union, BigQuery complains about column number mismatch, so I have to
-- make a temporary union table and select on it.
SELECT
*
FROM (
SELECT
-- Extract the logged JSON payload.
REGEXP_EXTRACT(textPayload, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$')
AS json
FROM `%PROJECT_ID%.%APPENGINE_LOGS_DATA_SET%.%APP_LOGS_TABLE%*`
WHERE
STARTS_WITH(textPayload, "FLOW-LOG-SIGNATURE-METADATA")
AND _TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%' AND '%LAST_DAY_OF_MONTH%')
UNION ALL (
SELECT
-- Extract the logged JSON payload.
REGEXP_EXTRACT(jsonPayload.message, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$')
AS json
FROM `%PROJECT_ID%.%GKE_LOGS_DATA_SET%.stderr_*`
WHERE
STARTS_WITH(jsonPayload.message, "FLOW-LOG-SIGNATURE-METADATA")
AND _TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%' AND '%LAST_DAY_OF_MONTH%')
)) AS regexes
-- Extract the logged JSON payload.
REGEXP_EXTRACT(jsonPayload.message, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$')
AS json
FROM `%PROJECT_ID%.gke_logs.stderr_*`
WHERE
STARTS_WITH(jsonPayload.message, "FLOW-LOG-SIGNATURE-METADATA")
AND _TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%' AND '%LAST_DAY_OF_MONTH%')
) AS regexes
JOIN
-- Unnest the JSON-parsed tlds.
UNNEST(regexes.tlds) AS tld

View File

@@ -13,7 +13,7 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
-- Query to fetch AppEngine and GKE request logs for the report month.
-- Query to fetch GKE request logs for the report month.
-- START_OF_MONTH and END_OF_MONTH should be in YYYYMM01 format.
@@ -23,13 +23,6 @@ FROM (
SELECT
jsonPayload.httrequest.requesturl AS requestPath
FROM
`%PROJECT_ID%.%GKE_LOGS_DATA_SET%.stderr_*`
WHERE
_TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%' AND '%LAST_DAY_OF_MONTH%')
UNION ALL (
SELECT
protoPayload.resource AS requestPath
FROM
`%PROJECT_ID%.%APPENGINE_LOGS_DATA_SET%.%REQUEST_TABLE%*`
`%PROJECT_ID%.gke_logs.stderr_*`
WHERE
_TABLE_SUFFIX BETWEEN '%FIRST_DAY_OF_MONTH%' AND '%LAST_DAY_OF_MONTH%')

View File

@@ -0,0 +1,212 @@
// Copyright 2025 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.flows;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.flows.CookieSessionMetadata.COOKIE_NAME;
import static google.registry.flows.CookieSessionMetadata.decode;
import static google.registry.flows.CookieSessionMetadata.encode;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableSet;
import google.registry.testing.FakeResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
/** Unit tests for {@link CookieSessionMetadata}. */
public class CookieSessionMetadataTest {
private HttpServletRequest request = mock(HttpServletRequest.class);
private FakeResponse response = new FakeResponse();
private CookieSessionMetadata cookieSessionMetadata = new CookieSessionMetadata(request);
@Test
void testNoCookie() {
assertThat(cookieSessionMetadata.getRegistrarId()).isNull();
assertThat(cookieSessionMetadata.getFailedLoginAttempts()).isEqualTo(0);
assertThat(cookieSessionMetadata.getServiceExtensionUris()).isEmpty();
}
@Test
void testCookieWithAllFields() {
when(request.getHeader("Cookie"))
.thenReturn(
"THIS_COOKIE=foo; SESSION_INFO="
+ encode(
"CookieSessionMetadata{clientId=test_registrar, failedLoginAttempts=5, "
+ " serviceExtensionUris=A|B|C}")
+ "; THAT_COOKIE=bar");
cookieSessionMetadata = new CookieSessionMetadata(request);
assertThat(cookieSessionMetadata.getRegistrarId()).isEqualTo("test_registrar");
assertThat(cookieSessionMetadata.getFailedLoginAttempts()).isEqualTo(5);
assertThat(cookieSessionMetadata.getServiceExtensionUris()).containsExactly("A", "B", "C");
}
@Test
void testCookieWithNullRegistrar() {
when(request.getHeader("Cookie"))
.thenReturn(
"SESSION_INFO="
+ encode(
"CookieSessionMetadata{clientId=null, failedLoginAttempts=5, "
+ " serviceExtensionUris=A|B|C}"));
cookieSessionMetadata = new CookieSessionMetadata(request);
assertThat(cookieSessionMetadata.getRegistrarId()).isNull();
assertThat(cookieSessionMetadata.getFailedLoginAttempts()).isEqualTo(5);
assertThat(cookieSessionMetadata.getServiceExtensionUris()).containsExactly("A", "B", "C");
}
@Test
void testCookieWithEmptyExtension() {
when(request.getHeader("Cookie"))
.thenReturn(
"SESSION_INFO="
+ encode(
"CookieSessionMetadata{clientId=test_registrar, failedLoginAttempts=5, "
+ " serviceExtensionUris=}"));
cookieSessionMetadata = new CookieSessionMetadata(request);
assertThat(cookieSessionMetadata.getRegistrarId()).isEqualTo("test_registrar");
assertThat(cookieSessionMetadata.getFailedLoginAttempts()).isEqualTo(5);
assertThat(cookieSessionMetadata.getServiceExtensionUris()).isEmpty();
}
@Test
void testCookieWithSingleExtension() {
when(request.getHeader("Cookie"))
.thenReturn(
"SESSION_INFO="
+ encode(
"CookieSessionMetadata{clientId=test_registrar, failedLoginAttempts=5, "
+ " serviceExtensionUris=Foo}"));
cookieSessionMetadata = new CookieSessionMetadata(request);
assertThat(cookieSessionMetadata.getRegistrarId()).isEqualTo("test_registrar");
assertThat(cookieSessionMetadata.getFailedLoginAttempts()).isEqualTo(5);
assertThat(cookieSessionMetadata.getServiceExtensionUris()).containsExactly("Foo");
}
@Test
void testIncrementFailedLoginAttempts() {
when(request.getHeader("Cookie"))
.thenReturn(
"SESSION_INFO="
+ encode(
"CookieSessionMetadata{clientId=test_registrar, failedLoginAttempts=5, "
+ " serviceExtensionUris=Foo}"));
cookieSessionMetadata = new CookieSessionMetadata(request);
cookieSessionMetadata.incrementFailedLoginAttempts();
assertThat(cookieSessionMetadata.getRegistrarId()).isEqualTo("test_registrar");
assertThat(cookieSessionMetadata.getFailedLoginAttempts()).isEqualTo(6);
assertThat(cookieSessionMetadata.getServiceExtensionUris()).containsExactly("Foo");
}
@Test
void testResetFailedLoginAttempts() {
when(request.getHeader("Cookie"))
.thenReturn(
"SESSION_INFO="
+ encode(
"CookieSessionMetadata{clientId=test_registrar, failedLoginAttempts=5, "
+ " serviceExtensionUris=Foo}"));
cookieSessionMetadata = new CookieSessionMetadata(request);
cookieSessionMetadata.resetFailedLoginAttempts();
assertThat(cookieSessionMetadata.getRegistrarId()).isEqualTo("test_registrar");
assertThat(cookieSessionMetadata.getFailedLoginAttempts()).isEqualTo(0);
assertThat(cookieSessionMetadata.getServiceExtensionUris()).containsExactly("Foo");
}
@Test
void testSetRegistrarId() {
when(request.getHeader("Cookie"))
.thenReturn(
"SESSION_INFO="
+ encode(
"CookieSessionMetadata{clientId=test_registrar, failedLoginAttempts=5, "
+ " serviceExtensionUris=Foo}"));
cookieSessionMetadata = new CookieSessionMetadata(request);
cookieSessionMetadata.setRegistrarId("new_registrar");
assertThat(cookieSessionMetadata.getRegistrarId()).isEqualTo("new_registrar");
assertThat(cookieSessionMetadata.getFailedLoginAttempts()).isEqualTo(5);
assertThat(cookieSessionMetadata.getServiceExtensionUris()).containsExactly("Foo");
}
@Test
void testSetExtensions() {
when(request.getHeader("Cookie"))
.thenReturn(
"SESSION_INFO="
+ encode(
"CookieSessionMetadata{clientId=test_registrar, failedLoginAttempts=5, "
+ " serviceExtensionUris=Foo}"));
cookieSessionMetadata = new CookieSessionMetadata(request);
cookieSessionMetadata.setServiceExtensionUris(ImmutableSet.of("Bar", "Baz", "foo:bar:baz-1.3"));
assertThat(cookieSessionMetadata.getRegistrarId()).isEqualTo("test_registrar");
assertThat(cookieSessionMetadata.getFailedLoginAttempts()).isEqualTo(5);
assertThat(cookieSessionMetadata.getServiceExtensionUris())
.containsExactly("Bar", "Baz", "foo:bar:baz-1.3");
}
@Test
void testSetEmptyExtensions() {
when(request.getHeader("Cookie"))
.thenReturn(
"SESSION_INFO="
+ encode(
"CookieSessionMetadata{clientId=test_registrar, failedLoginAttempts=5, "
+ " serviceExtensionUris=Foo}"));
cookieSessionMetadata = new CookieSessionMetadata(request);
cookieSessionMetadata.setServiceExtensionUris(ImmutableSet.of());
assertThat(cookieSessionMetadata.getRegistrarId()).isEqualTo("test_registrar");
assertThat(cookieSessionMetadata.getFailedLoginAttempts()).isEqualTo(5);
assertThat(cookieSessionMetadata.getServiceExtensionUris()).isEmpty();
}
@Test
void testInvalidate() {
when(request.getHeader("Cookie"))
.thenReturn(
"SESSION_INFO="
+ encode(
"CookieSessionMetadata{clientId=test_registrar, failedLoginAttempts=5, "
+ " serviceExtensionUris=Foo}"));
cookieSessionMetadata = new CookieSessionMetadata(request);
cookieSessionMetadata.invalidate();
assertThat(cookieSessionMetadata.getRegistrarId()).isNull();
assertThat(cookieSessionMetadata.getFailedLoginAttempts()).isEqualTo(0);
assertThat(cookieSessionMetadata.getServiceExtensionUris()).isEmpty();
}
@Test
void testSave() {
cookieSessionMetadata.save(response);
String value =
decode(
response.getHeaders().get("Set-Cookie").toString().substring(COOKIE_NAME.length() + 1));
assertThat(value)
.isEqualTo(
"CookieSessionMetadata{clientId=null, failedLoginAttempts=0, serviceExtensionUris=}");
cookieSessionMetadata.setRegistrarId("new_registrar");
cookieSessionMetadata.setServiceExtensionUris(ImmutableSet.of("Bar", "Baz"));
cookieSessionMetadata.incrementFailedLoginAttempts();
cookieSessionMetadata.save(response);
value =
decode(
response.getHeaders().get("Set-Cookie").toString().substring(COOKIE_NAME.length() + 1));
assertThat(value)
.isEqualTo(
"CookieSessionMetadata{clientId=new_registrar, failedLoginAttempts=1,"
+ " serviceExtensionUris=Bar|Baz}");
}
}

View File

@@ -12,17 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.flows;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.US_ASCII;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import google.registry.testing.FakeHttpSession;
import com.google.common.io.BaseEncoding;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -36,18 +38,22 @@ class EppTlsActionTest {
EppTlsAction action = new EppTlsAction();
action.inputXmlBytes = INPUT_XML_BYTES;
action.tlsCredentials = mock(TlsCredentials.class);
action.session = new FakeHttpSession();
action.session.setAttribute("REGISTRAR_ID", "ClientIdentifier");
action.request = mock(HttpServletRequest.class);
when(action.request.getHeader("Cookie"))
.thenReturn(
"SESSION_INFO="
+ BaseEncoding.base64Url().encode("clientId=ClientIdentifier".getBytes(US_ASCII)));
action.eppRequestHandler = mock(EppRequestHandler.class);
action.run();
ArgumentCaptor<SessionMetadata> captor = ArgumentCaptor.forClass(SessionMetadata.class);
verify(action.eppRequestHandler).executeEpp(
captor.capture(),
same(action.tlsCredentials),
eq(EppRequestSource.TLS),
eq(false),
eq(false),
eq(INPUT_XML_BYTES));
verify(action.eppRequestHandler)
.executeEpp(
captor.capture(),
same(action.tlsCredentials),
eq(EppRequestSource.TLS),
eq(false),
eq(false),
eq(INPUT_XML_BYTES));
assertThat(captor.getValue().getRegistrarId()).isEqualTo("ClientIdentifier");
}
}

View File

@@ -0,0 +1,42 @@
// Copyright 2025 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.rdap;
import static com.google.common.truth.Truth.assertThat;
import google.registry.testing.FakeResponse;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/** Tests for {@link RdapEmptyAction}. */
public class RdapEmptyActionTest {
private FakeResponse fakeResponse;
private RdapEmptyAction action;
@BeforeEach
void beforeEach() {
fakeResponse = new FakeResponse();
action = new RdapEmptyAction(fakeResponse);
}
@Test
void testRedirect() {
action.run();
assertThat(fakeResponse.getStatus()).isEqualTo(HttpServletResponse.SC_FOUND);
assertThat(fakeResponse.getPayload()).isEqualTo("Redirected to /rdap/help");
}
}

View File

@@ -53,8 +53,8 @@ import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link WhoisRegistrarFieldsAction}. */
public class WhoisRegistrarFieldsActionTest {
/** Tests for {@link RdapRegistrarFieldsAction}. */
public class RdapRegistrarFieldsActionTest {
private ConsoleApiParams consoleApiParams;
private static final Gson GSON = RequestModule.provideGson();
@@ -89,8 +89,6 @@ public class WhoisRegistrarFieldsActionTest {
@Test
void testSuccess_setsAllFields() throws Exception {
Registrar oldRegistrar = Registrar.loadRequiredRegistrarCached("TheRegistrar");
assertThat(oldRegistrar.getWhoisServer()).isEqualTo("whois.nic.fakewhois.example");
assertThat(oldRegistrar.getUrl()).isEqualTo("http://my.fake.url");
ImmutableMap<String, Object> addressMap =
ImmutableMap.of(
"street",
@@ -105,33 +103,26 @@ public class WhoisRegistrarFieldsActionTest {
"CA");
uiRegistrarMap.putAll(
ImmutableMap.of(
"whoisServer",
"whois.nic.google",
"icannReferralEmail",
"lol@sloth.test",
"phoneNumber",
"+1.4155552671",
"faxNumber",
"+1.4155552672",
"url",
"\"https://newurl.example\"",
"localizedAddress",
"{\"street\": [\"123 Fake St\"], \"city\": \"Fakeville\", \"state\":"
+ " \"NL\", \"zip\": \"10011\", \"countryCode\": \"CA\"}"));
WhoisRegistrarFieldsAction action = createAction();
RdapRegistrarFieldsAction action = createAction();
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_OK);
Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get(); // skip cache
assertThat(newRegistrar.getWhoisServer()).isEqualTo("whois.nic.google");
assertThat(newRegistrar.getUrl()).isEqualTo("https://newurl.example");
assertThat(newRegistrar.getLocalizedAddress().toJsonMap()).isEqualTo(addressMap);
assertThat(newRegistrar.getPhoneNumber()).isEqualTo("+1.4155552671");
assertThat(newRegistrar.getFaxNumber()).isEqualTo("+1.4155552672");
// the non-changed fields should be the same
assertAboutImmutableObjects()
.that(newRegistrar)
.isEqualExceptFields(
oldRegistrar, "whoisServer", "url", "localizedAddress", "phoneNumber", "faxNumber");
.isEqualExceptFields(oldRegistrar, "localizedAddress", "phoneNumber", "faxNumber");
SimpleConsoleUpdateHistory history = loadSingleton(SimpleConsoleUpdateHistory.class).get();
assertThat(history.getType()).isEqualTo(ConsoleUpdateHistory.Type.REGISTRAR_UPDATE);
assertThat(history.getDescription()).hasValue("TheRegistrar");
@@ -151,7 +142,7 @@ public class WhoisRegistrarFieldsActionTest {
.build())
.build());
uiRegistrarMap.put("registrarId", "NewRegistrar");
WhoisRegistrarFieldsAction action = createAction(onlyTheRegistrar);
RdapRegistrarFieldsAction action = createAction(onlyTheRegistrar);
action.run();
assertThat(((FakeResponse) consoleApiParams.response()).getStatus()).isEqualTo(SC_FORBIDDEN);
// should be no change
@@ -162,17 +153,17 @@ public class WhoisRegistrarFieldsActionTest {
return AuthResult.createUser(DatabaseHelper.createAdminUser("email@email.example"));
}
private WhoisRegistrarFieldsAction createAction() throws IOException {
private RdapRegistrarFieldsAction createAction() throws IOException {
return createAction(defaultUserAuth());
}
private WhoisRegistrarFieldsAction createAction(AuthResult authResult) throws IOException {
private RdapRegistrarFieldsAction createAction(AuthResult authResult) throws IOException {
consoleApiParams = ConsoleApiParamsUtils.createFake(authResult);
when(consoleApiParams.request().getMethod()).thenReturn(Action.Method.POST.toString());
doReturn(new BufferedReader(new StringReader(uiRegistrarMap.toString())))
.when(consoleApiParams.request())
.getReader();
return new WhoisRegistrarFieldsAction(
return new RdapRegistrarFieldsAction(
consoleApiParams,
registrarAccessor,
ConsoleModule.provideRegistrar(

View File

@@ -101,7 +101,7 @@ public class ConsoleScreenshotTest extends WebDriverTestCase {
driver.diffPage("noRegistrarSelected");
selectRegistrar();
driver.diffPage("registrarSelected_contacts");
driver.findElement(By.cssSelector("a[routerLink=\"whois\"]")).click();
driver.findElement(By.cssSelector("a[routerLink=\"rdap\"]")).click();
Thread.sleep(500);
driver.diffPage("registrarSelected_whois");
driver.findElement(By.cssSelector("a[routerLink=\"security\"]")).click();

View File

@@ -1,17 +1,19 @@
SERVICE PATH CLASS METHODS OK MIN USER_POLICY
FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN
CONSOLE /console-api/bulk-domain ConsoleBulkDomainAction POST n USER PUBLIC
CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC
CONSOLE /console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC
CONSOLE /console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC
CONSOLE /console-api/eppPassword ConsoleEppPasswordAction POST n USER PUBLIC
CONSOLE /console-api/ote ConsoleOteAction GET,POST n USER PUBLIC
CONSOLE /console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC
CONSOLE /console-api/registrars RegistrarsAction GET,POST n USER PUBLIC
CONSOLE /console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC
CONSOLE /console-api/registry-lock-verify ConsoleRegistryLockVerifyAction GET n USER PUBLIC
CONSOLE /console-api/settings/contacts ContactAction GET,POST n USER PUBLIC
CONSOLE /console-api/settings/security SecurityAction POST n USER PUBLIC
CONSOLE /console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n USER PUBLIC
CONSOLE /console-api/userdata ConsoleUserDataAction GET n USER PUBLIC
CONSOLE /console-api/users ConsoleUsersAction GET,POST,DELETE,PUT n USER PUBLIC
SERVICE PATH CLASS METHODS OK MIN USER_POLICY
FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN
FRONTEND /ready/frontend ReadinessProbeActionFrontend GET n NONE PUBLIC
CONSOLE /console-api/bulk-domain ConsoleBulkDomainAction POST n USER PUBLIC
CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC
CONSOLE /console-api/domain-list ConsoleDomainListAction GET n USER PUBLIC
CONSOLE /console-api/dum-download ConsoleDumDownloadAction GET n USER PUBLIC
CONSOLE /console-api/eppPassword ConsoleEppPasswordAction POST n USER PUBLIC
CONSOLE /console-api/ote ConsoleOteAction GET,POST n USER PUBLIC
CONSOLE /console-api/registrar ConsoleUpdateRegistrarAction POST n USER PUBLIC
CONSOLE /console-api/registrars RegistrarsAction GET,POST n USER PUBLIC
CONSOLE /console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC
CONSOLE /console-api/registry-lock-verify ConsoleRegistryLockVerifyAction GET n USER PUBLIC
CONSOLE /console-api/settings/contacts ContactAction GET,POST n USER PUBLIC
CONSOLE /console-api/settings/rdap-fields RdapRegistrarFieldsAction POST n USER PUBLIC
CONSOLE /console-api/settings/security SecurityAction POST n USER PUBLIC
CONSOLE /console-api/userdata ConsoleUserDataAction GET n USER PUBLIC
CONSOLE /console-api/users ConsoleUsersAction GET,POST,DELETE,PUT n USER PUBLIC
CONSOLE /ready/console ReadinessProbeConsoleAction GET n NONE PUBLIC

View File

@@ -1,6 +1,7 @@
SERVICE PATH CLASS METHODS OK MIN USER_POLICY
PUBAPI /_dr/whois WhoisAction POST n APP ADMIN
PUBAPI /check CheckApiAction GET n NONE PUBLIC
PUBAPI /rdap/ RdapEmptyAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/autnum/(*) RdapAutnumAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/domain/(*) RdapDomainAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/domains RdapDomainSearchAction GET,HEAD n NONE PUBLIC
@@ -10,4 +11,5 @@ PUBAPI /rdap/help(*) RdapHelpAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/ip/(*) RdapIpAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/nameserver/(*) RdapNameserverAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/nameservers RdapNameserverSearchAction GET,HEAD n NONE PUBLIC
PUBAPI /ready/pubapi ReadinessProbeActionPubApi GET n NONE PUBLIC
PUBAPI /whois/(*) WhoisHttpAction GET n NONE PUBLIC

View File

@@ -1,5 +1,6 @@
SERVICE PATH CLASS METHODS OK MIN USER_POLICY
FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN
FRONTEND /ready/frontend ReadinessProbeActionFrontend GET n NONE PUBLIC
BACKEND /_dr/admin/createGroups CreateGroupsAction POST n APP ADMIN
BACKEND /_dr/admin/list/domains ListDomainsAction GET,POST n APP ADMIN
BACKEND /_dr/admin/list/hosts ListHostsAction GET,POST n APP ADMIN
@@ -56,6 +57,7 @@ BACKEND /_dr/task/uploadBsaUnavailableNames UploadBsaUnavailable
BACKEND /_dr/task/wipeOutContactHistoryPii WipeOutContactHistoryPiiAction GET n APP ADMIN
PUBAPI /_dr/whois WhoisAction POST n APP ADMIN
PUBAPI /check CheckApiAction GET n NONE PUBLIC
PUBAPI /rdap/ RdapEmptyAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/autnum/(*) RdapAutnumAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/domain/(*) RdapDomainAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/domains RdapDomainSearchAction GET,HEAD n NONE PUBLIC
@@ -65,6 +67,7 @@ PUBAPI /rdap/help(*) RdapHelpAction
PUBAPI /rdap/ip/(*) RdapIpAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/nameserver/(*) RdapNameserverAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/nameservers RdapNameserverSearchAction GET,HEAD n NONE PUBLIC
PUBAPI /ready/pubapi ReadinessProbeActionPubApi GET n NONE PUBLIC
PUBAPI /whois/(*) WhoisHttpAction GET n NONE PUBLIC
CONSOLE /console-api/bulk-domain ConsoleBulkDomainAction POST n USER PUBLIC
CONSOLE /console-api/domain ConsoleDomainGetAction GET n USER PUBLIC
@@ -77,7 +80,8 @@ CONSOLE /console-api/registrars RegistrarsAction
CONSOLE /console-api/registry-lock ConsoleRegistryLockAction GET,POST n USER PUBLIC
CONSOLE /console-api/registry-lock-verify ConsoleRegistryLockVerifyAction GET n USER PUBLIC
CONSOLE /console-api/settings/contacts ContactAction GET,POST n USER PUBLIC
CONSOLE /console-api/settings/rdap-fields RdapRegistrarFieldsAction POST n USER PUBLIC
CONSOLE /console-api/settings/security SecurityAction POST n USER PUBLIC
CONSOLE /console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n USER PUBLIC
CONSOLE /console-api/userdata ConsoleUserDataAction GET n USER PUBLIC
CONSOLE /console-api/users ConsoleUsersAction GET,POST,DELETE,PUT n USER PUBLIC
CONSOLE /ready/console ReadinessProbeConsoleAction GET n NONE PUBLIC

View File

@@ -16,7 +16,7 @@
-- Determine the number of attempted adds each registrar made.
-- Since the specification requests all 'attempted' adds, we regex the
-- monthly App Engine logs, searching for all create commands and associating
-- monthly GKE logs, searching for all create commands and associating
-- them with their corresponding registrars.
-- Example log generated by FlowReporter in App Engine and GKE logs:
@@ -27,7 +27,7 @@
-- ,"targetId":"","targetIds":[],"tld":"",
-- "tlds":[],"icannActivityReportField":""}
-- This outer select just converts the registrar's clientId to their name.
-- This outer select just converts the registrar's ID to their name.
SELECT
tld,
registrar_table.registrar_name AS registrar_name,
@@ -38,34 +38,20 @@ FROM (
JSON_EXTRACT_SCALAR(json, '$.tld') AS tld,
JSON_EXTRACT_SCALAR(json, '$.clientId') AS clientId,
COUNT(json) AS count
FROM ((
-- Extract JSON metadata package from monthly logs
FROM (
-- Extract JSON metadata package from monthly logs
SELECT
REGEXP_EXTRACT(textPayload, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$') AS json
REGEXP_EXTRACT(jsonPayload.message, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$') AS json
FROM
`domain-registry-alpha.appengine_logs._var_log_app_*`
`domain-registry-alpha.gke_logs.stderr_*`
WHERE
_TABLE_SUFFIX BETWEEN '20170901'
AND '20170930'
AND STARTS_WITH(textPayload, "FLOW-LOG-SIGNATURE-METADATA")
AND STARTS_WITH(jsonPayload.message, "FLOW-LOG-SIGNATURE-METADATA")
-- Look for domain creates
AND REGEXP_CONTAINS(textPayload, r'"commandType":"create","resourceType":"domain"')
AND REGEXP_CONTAINS(jsonPayload.message, r'"commandType":"create","resourceType":"domain"')
-- Filter prober data
AND NOT REGEXP_CONTAINS(textPayload, r'"prober-[a-z]{2}-((any)|(canary))"'))
UNION ALL (
-- Extract JSON metadata package from monthly logs
SELECT
REGEXP_EXTRACT(jsonPayload.message, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$') AS json
FROM
`domain-registry-alpha.gke_logs.stderr_*`
WHERE
_TABLE_SUFFIX BETWEEN '20170901'
AND '20170930'
AND STARTS_WITH(jsonPayload.message, "FLOW-LOG-SIGNATURE-METADATA")
-- Look for domain creates
AND REGEXP_CONTAINS(jsonPayload.message, r'"commandType":"create","resourceType":"domain"')
-- Filter prober data
AND NOT REGEXP_CONTAINS(jsonPayload.message, r'"prober-[a-z]{2}-((any)|(canary))"')))
AND NOT REGEXP_CONTAINS(jsonPayload.message, r'"prober-[a-z]{2}-((any)|(canary))"'))
GROUP BY
tld,
clientId ) AS logs_table

View File

@@ -15,7 +15,7 @@
-- Query FlowReporter JSON log messages and calculate SRS metrics.
-- We use ugly regex's over the monthly appengine logs to determine how many
-- We use ugly regexes over the monthly GKE logs to determine how many
-- EPP requests we received for each command. For example:
-- {"commandType":"check"...,"targetIds":["ais.a.how"],
-- "tld":"","tlds":["a.how"],"icannActivityReportField":"srs-dom-check"}
@@ -35,30 +35,15 @@ FROM (
JSON_EXTRACT_SCALAR(json,
'$.icannActivityReportField') AS activityReportField
FROM (
-- For reasons that I don't understand, if I directly select the three columns
-- from the union, BigQuery complains about column number mismatch, so I have to
-- make a temporary union table and select on it.
SELECT
*
FROM (
SELECT
-- Extract the logged JSON payload.
REGEXP_EXTRACT(textPayload, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$')
AS json
FROM `domain-registry-alpha.appengine_logs._var_log_app_*`
WHERE
STARTS_WITH(textPayload, "FLOW-LOG-SIGNATURE-METADATA")
AND _TABLE_SUFFIX BETWEEN '20170901' AND '20170930')
UNION ALL (
SELECT
-- Extract the logged JSON payload.
REGEXP_EXTRACT(jsonPayload.message, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$')
AS json
FROM `domain-registry-alpha.gke_logs.stderr_*`
WHERE
STARTS_WITH(jsonPayload.message, "FLOW-LOG-SIGNATURE-METADATA")
AND _TABLE_SUFFIX BETWEEN '20170901' AND '20170930')
)) AS regexes
-- Extract the logged JSON payload.
REGEXP_EXTRACT(jsonPayload.message, r'FLOW-LOG-SIGNATURE-METADATA: (.*)\n?$')
AS json
FROM `domain-registry-alpha.gke_logs.stderr_*`
WHERE
STARTS_WITH(jsonPayload.message, "FLOW-LOG-SIGNATURE-METADATA")
AND _TABLE_SUFFIX BETWEEN '20170901' AND '20170930')
) AS regexes
JOIN
-- Unnest the JSON-parsed tlds.
UNNEST(regexes.tlds) AS tld

View File

@@ -13,7 +13,7 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
-- Query to fetch AppEngine and GKE request logs for the report month.
-- Query to fetch GKE request logs for the report month.
-- START_OF_MONTH and END_OF_MONTH should be in YYYYMM01 format.
@@ -26,10 +26,3 @@ FROM (
`domain-registry-alpha.gke_logs.stderr_*`
WHERE
_TABLE_SUFFIX BETWEEN '20170901' AND '20170930')
UNION ALL (
SELECT
protoPayload.resource AS requestPath
FROM
`domain-registry-alpha.appengine_logs.appengine_googleapis_com_request_log_*`
WHERE
_TABLE_SUFFIX BETWEEN '20170901' AND '20170930')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -50,7 +50,7 @@ tasks.register('stage') {
}
tasks.register('buildNomulusImage', Exec) {
commandLine 'docker', 'build', '-t', 'nomulus', '.'
commandLine 'docker', 'build', '-t', 'nomulus', '.', '--pull'
dependsOn(tasks.named('stage'))
}
@@ -137,3 +137,4 @@ tasks.register('getEndpoints', Exec) {
}
project.build.dependsOn(tasks.named('buildNomulusImage'))
rootProject.deploy.dependsOn(tasks.named('deployNomulus'))

View File

@@ -39,7 +39,7 @@ do
sed s/ENVIRONMENT/"${environment}"/g | \
sed s/PROXY_ENV/"${environment}"/g | \
sed s/EPP/"epp"/g | \
kubectl apply -f -
kubectl apply --grace-period=1 -f -
kubectl rollout restart deployment/${service}
# canary
sed s/GCP_PROJECT/"${project}"/g "./kubernetes/nomulus-${service}.yaml" | \
@@ -47,7 +47,10 @@ do
sed s/PROXY_ENV/"${environment}_canary"/g | \
sed s/EPP/"epp-canary"/g | \
sed s/"${service}"/"${service}-canary"/g | \
kubectl apply -f -
# Undo prober endpoint replacement done in the previous line.
# The link should stay as /ready/${service}.
sed s/"ready\/${service}-canary"/"ready\/${service}"/g | \
kubectl apply --grace-period=1 -f -
kubectl rollout restart deployment/${service}-canary
done
kubectl apply -f "./kubernetes/gateway/nomulus-gateway.yaml"
@@ -62,15 +65,4 @@ do
kubectl apply -f -
done
# Restart proxies
while read line
do
parts=(${line})
echo "Updating cluster ${parts[0]} in location ${parts[1]}..."
gcloud container clusters get-credentials ${parts[0]} \
--project ${project} --location ${parts[1]}
kubectl rollout restart deployment/proxy-deployment
kubectl rollout restart deployment/proxy-deployment-canary
done < <(gcloud container clusters list --project ${project} | grep proxy-cluster)
kubectl config use-context "$current_context"

View File

@@ -20,6 +20,15 @@ spec:
ports:
- containerPort: 8080
name: http
startupProbe:
httpGet:
port: 8080
path: /ready/console
initialDelaySeconds: 1
timeoutSeconds: 60
successThreshold: 1
failureThreshold: 3
periodSeconds: 30
resources:
requests:
# explicit pod-slots 0 is required in order to downgrade node

View File

@@ -26,7 +26,7 @@ spec:
resources:
requests:
cpu: "100m"
memory: "512Mi"
memory: "1Gi"
args: [ENVIRONMENT]
env:
- name: POD_ID
@@ -82,8 +82,8 @@ spec:
apiVersion: apps/v1
kind: Deployment
name: frontend
minReplicas: 15
maxReplicas: 15
minReplicas: 5
maxReplicas: 20
metrics:
- type: Resource
resource:

View File

@@ -20,13 +20,22 @@ spec:
ports:
- containerPort: 8080
name: http
startupProbe:
httpGet:
port: 8080
path: /ready/pubapi
initialDelaySeconds: 1
timeoutSeconds: 60
successThreshold: 1
failureThreshold: 3
periodSeconds: 30
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: "100m"
memory: "512Mi"
memory: "1Gi"
limits:
# explicit pod-slots 0 is required in order to downgrade node
# class from performance, which has implicit pod-slots 1
@@ -62,12 +71,12 @@ spec:
minReplicas: 5
maxReplicas: 15
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 100
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 100
---
apiVersion: v1
kind: Service
@@ -77,9 +86,9 @@ spec:
selector:
service: pubapi
ports:
- port: 80
targetPort: http
name: http
- port: 80
targetPort: http
name: http
---
apiVersion: net.gke.io/v1
kind: ServiceExport

View File

@@ -42,7 +42,6 @@ import java.security.cert.CertificateException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
@@ -272,17 +271,11 @@ class SslServerInitializerTest {
getClientHandler(
sslProvider, serverSsc.cert(), clientSsc.key(), clientSsc.cert(), "TLSv1.1", null));
ImmutableList<Integer> jdkVersion =
Arrays.stream(System.getProperty("java.version").split("\\."))
.map(Integer::parseInt)
.collect(ImmutableList.toImmutableList());
// In JDK v11.0.11 and above, TLS 1.1 is not supported anymore, in which case attempting to
// connect with TLS 1.1 results in a ClosedChannelException instead of a SSLHandShakeException.
// See https://www.oracle.com/java/technologies/javase/11-0-11-relnotes.html#JDK-8202343
Class<? extends Exception> rootCause =
sslProvider == SslProvider.JDK
&& compareSemanticVersion(jdkVersion, ImmutableList.of(11, 0, 11))
? ClosedChannelException.class
: SSLHandshakeException.class;

View File

@@ -15,7 +15,7 @@
createUberJar('deployJar', 'proxy_server', 'google.registry.proxy.ProxyServer')
task buildProxyImage(dependsOn: deployJar, type: Exec) {
commandLine 'docker', 'build', '-t', 'proxy', '.'
commandLine 'docker', 'build', '-t', 'proxy', '.', '--pull'
}
task tagProxyImage(dependsOn: buildProxyImage, type: Exec) {

View File

@@ -211,7 +211,10 @@ steps:
sed s/ENVIRONMENT/${env}/g | \
sed s/PROXY_ENV/"${env}_canary"/g | \
sed s/EPP/"epp-canary"/g | \
sed s/${service}/${service}-canary/g \
sed s/${service}/${service}-canary/g | \
# Undo prober endpoint replacement done in the previous line.
# The link should stay as /ready/${service}.
sed s/"ready\/${service}-canary"/"ready\/${service}"/g \
> ./jetty/kubernetes/nomulus-${env}-${service}-canary.yaml
# Proxy '--log' flag does not work on production.
if [ ${env} == production ]