mirror of
https://github.com/google/nomulus
synced 2026-02-11 23:31:37 +00:00
Compare commits
23 Commits
nomulus-20
...
tlds-20240
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
811b385544 | ||
|
|
3f5c9d1246 | ||
|
|
5315752bc0 | ||
|
|
4eee7b8c0d | ||
|
|
ecb39d5899 | ||
|
|
42b508427b | ||
|
|
20b5b43501 | ||
|
|
08285f5de7 | ||
|
|
fb4c5b457d | ||
|
|
781c212275 | ||
|
|
c73f7a6bd3 | ||
|
|
8d793b2349 | ||
|
|
55d5f8c6f8 | ||
|
|
9006312253 | ||
|
|
e5e2370923 | ||
|
|
b3b0efd47e | ||
|
|
e82cbe60a9 | ||
|
|
923bc13e3a | ||
|
|
4893ea307b | ||
|
|
01f868cefc | ||
|
|
1b0919eaff | ||
|
|
92b23bac16 | ||
|
|
cc9b3f5965 |
@@ -0,0 +1,43 @@
|
||||
// Copyright 2023 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.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.Iterators.partition;
|
||||
import static com.google.common.collect.Iterators.transform;
|
||||
import static com.google.common.collect.Streams.stream;
|
||||
import static java.lang.Math.min;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/** Utilities for breaking up a {@link Stream} into batches. */
|
||||
public final class BatchedStreams {
|
||||
|
||||
static final int MAX_BATCH = 1024 * 1024;
|
||||
|
||||
private BatchedStreams() {}
|
||||
|
||||
/**
|
||||
* Transform a flat {@link Stream} into a {@code Stream} of batches.
|
||||
*
|
||||
* <p>Closing the returned stream does not close the original stream.
|
||||
*/
|
||||
public static <T> Stream<ImmutableList<T>> toBatches(Stream<T> stream, int batchSize) {
|
||||
checkArgument(batchSize > 0, "batchSize must be a positive integer.");
|
||||
return stream(
|
||||
transform(partition(stream.iterator(), min(MAX_BATCH, batchSize)), ImmutableList::copyOf));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2023 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 google.registry.util.BatchedStreams.toBatches;
|
||||
import static java.util.stream.Collectors.counting;
|
||||
import static java.util.stream.Collectors.groupingBy;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.stream.IntStream;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link BatchedStreams}. */
|
||||
public class BatchedStreamsTest {
|
||||
|
||||
@Test
|
||||
void invalidBatchSize() {
|
||||
assertThat(assertThrows(IllegalArgumentException.class, () -> toBatches(Stream.of(), 0)))
|
||||
.hasMessageThat()
|
||||
.contains("must be a positive integer");
|
||||
}
|
||||
|
||||
@Test
|
||||
void batch_success() {
|
||||
// 900_002 elements -> 900 1K-batches + 1 2-element-batch
|
||||
Stream<Integer> data = IntStream.rangeClosed(0, 900_001).boxed();
|
||||
assertThat(
|
||||
toBatches(data, 1000).map(ImmutableList::size).collect(groupingBy(x -> x, counting())))
|
||||
.containsExactly(1000, 900L, 2, 1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void batch_partialBatch() {
|
||||
Stream<Integer> data = Stream.of(1, 2, 3);
|
||||
assertThat(
|
||||
toBatches(data, 1000).map(ImmutableList::size).collect(groupingBy(x -> x, counting())))
|
||||
.containsExactly(3, 1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void batch_truncateBatchSize() {
|
||||
// 2M elements -> 2 1M-batches despite the user-specified 2M batch size.
|
||||
Stream<Integer> data = IntStream.range(0, 1024 * 2048).boxed();
|
||||
assertThat(
|
||||
toBatches(data, 2_000_000)
|
||||
.map(ImmutableList::size)
|
||||
.collect(groupingBy(x -> x, counting())))
|
||||
.containsExactly(1024 * 1024, 2L);
|
||||
}
|
||||
}
|
||||
@@ -49,15 +49,14 @@ import { SettingsWidgetComponent } from './home/widgets/settingsWidget.component
|
||||
import { UserDataService } from './shared/services/userData.service';
|
||||
import WhoisComponent from './settings/whois/whois.component';
|
||||
import { SnackBarModule } from './snackbar.module';
|
||||
import {
|
||||
RegistrarDetailsComponent,
|
||||
RegistrarDetailsWrapperComponent,
|
||||
} from './registrar/registrarDetails.component';
|
||||
import { RegistrarDetailsComponent } from './registrar/registrarDetails.component';
|
||||
import { DomainListComponent } from './domains/domainList.component';
|
||||
import { DialogBottomSheetWrapper } from './shared/components/dialogBottomSheet.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
DialogBottomSheetWrapper,
|
||||
BillingWidgetComponent,
|
||||
ContactDetailsDialogComponent,
|
||||
ContactWidgetComponent,
|
||||
@@ -70,7 +69,6 @@ import { DomainListComponent } from './domains/domainList.component';
|
||||
PromotionsWidgetComponent,
|
||||
RegistrarComponent,
|
||||
RegistrarDetailsComponent,
|
||||
RegistrarDetailsWrapperComponent,
|
||||
RegistrarSelectorComponent,
|
||||
ResourcesWidgetComponent,
|
||||
SecurityComponent,
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<div class="console-domains">
|
||||
<mat-form-field>
|
||||
<mat-label>Filter</mat-label>
|
||||
<input matInput (keyup)="applyFilter($event)" #input />
|
||||
<input
|
||||
type="search"
|
||||
matInput
|
||||
[(ngModel)]="searchTerm"
|
||||
(ngModelChange)="sendInput()"
|
||||
#input
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<div *ngIf="isLoading; else domains_content" class="console-domains__loading">
|
||||
|
||||
@@ -18,6 +18,7 @@ import { BackendService } from '../shared/services/backend.service';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { RegistrarService } from '../registrar/registrar.service';
|
||||
import { Domain, DomainListService } from './domainList.service';
|
||||
import { Subject, debounceTime } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-domain-list',
|
||||
@@ -27,6 +28,7 @@ import { Domain, DomainListService } from './domainList.service';
|
||||
})
|
||||
export class DomainListComponent {
|
||||
public static PATH = 'domain-list';
|
||||
private readonly DEBOUNCE_MS = 500;
|
||||
|
||||
displayedColumns: string[] = [
|
||||
'domainName',
|
||||
@@ -38,6 +40,9 @@ export class DomainListComponent {
|
||||
dataSource: MatTableDataSource<Domain> = new MatTableDataSource();
|
||||
isLoading = true;
|
||||
|
||||
searchTermSubject = new Subject<string>();
|
||||
searchTerm?: string;
|
||||
|
||||
pageNumber?: number;
|
||||
resultsPerPage = 50;
|
||||
totalResults?: number;
|
||||
@@ -52,13 +57,28 @@ export class DomainListComponent {
|
||||
|
||||
ngOnInit() {
|
||||
this.dataSource.paginator = this.paginator;
|
||||
// Don't spam the server unnecessarily while the user is typing
|
||||
this.searchTermSubject
|
||||
.pipe(debounceTime(this.DEBOUNCE_MS))
|
||||
.subscribe((searchTermValue) => {
|
||||
this.reloadData();
|
||||
});
|
||||
this.reloadData();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.searchTermSubject.complete();
|
||||
}
|
||||
|
||||
reloadData() {
|
||||
this.isLoading = true;
|
||||
this.domainListService
|
||||
.retrieveDomains(this.pageNumber, this.resultsPerPage, this.totalResults)
|
||||
.retrieveDomains(
|
||||
this.pageNumber,
|
||||
this.resultsPerPage,
|
||||
this.totalResults,
|
||||
this.searchTerm
|
||||
)
|
||||
.subscribe((domainListResult) => {
|
||||
this.dataSource.data = domainListResult.domains;
|
||||
this.totalResults = domainListResult.totalResults;
|
||||
@@ -66,10 +86,8 @@ export class DomainListComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/** TODO: the backend will need to accept a filter string. */
|
||||
applyFilter(event: KeyboardEvent) {
|
||||
// const filterValue = (event.target as HTMLInputElement).value;
|
||||
this.reloadData();
|
||||
sendInput() {
|
||||
this.searchTermSubject.next(this.searchTerm!);
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent) {
|
||||
|
||||
@@ -47,7 +47,8 @@ export class DomainListService {
|
||||
retrieveDomains(
|
||||
pageNumber?: number,
|
||||
resultsPerPage?: number,
|
||||
totalResults?: number
|
||||
totalResults?: number,
|
||||
searchTerm?: string
|
||||
) {
|
||||
return this.backendService
|
||||
.getDomains(
|
||||
@@ -55,7 +56,8 @@ export class DomainListService {
|
||||
this.checkpointTime,
|
||||
pageNumber,
|
||||
resultsPerPage,
|
||||
totalResults
|
||||
totalResults,
|
||||
searchTerm
|
||||
)
|
||||
.pipe(
|
||||
tap((domainListResult: DomainListResult) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="registrarDetails">
|
||||
<div class="registrarDetails" *ngIf="registrarInEdit">
|
||||
<h3 mat-dialog-title>Edit Registrar: {{ registrarInEdit.registrarId }}</h3>
|
||||
<div mat-dialog-content>
|
||||
<form (ngSubmit)="saveAndClose($event)">
|
||||
<form (ngSubmit)="saveAndClose()">
|
||||
<mat-form-field class="registrarDetails__input">
|
||||
<mat-label>Registry Lock:</mat-label>
|
||||
<mat-select
|
||||
@@ -32,7 +32,7 @@
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onCancel($event)">Cancel</button>
|
||||
<button mat-button (click)="this.params?.close()">Cancel</button>
|
||||
<button type="submit" mat-button color="primary">Save</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
|
||||
@@ -12,61 +12,38 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Injector } from '@angular/core';
|
||||
import { Component } from '@angular/core';
|
||||
import { Registrar, RegistrarService } from './registrar.service';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import {
|
||||
MAT_BOTTOM_SHEET_DATA,
|
||||
MatBottomSheet,
|
||||
MatBottomSheetRef,
|
||||
} from '@angular/material/bottom-sheet';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialog,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { DialogBottomSheetContent } from '../shared/components/dialogBottomSheet.component';
|
||||
|
||||
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
|
||||
type RegistrarDetailsParams = {
|
||||
close: Function;
|
||||
data: {
|
||||
registrar: Registrar;
|
||||
};
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar-details',
|
||||
templateUrl: './registrarDetails.component.html',
|
||||
styleUrls: ['./registrarDetails.component.scss'],
|
||||
})
|
||||
export class RegistrarDetailsComponent {
|
||||
export class RegistrarDetailsComponent implements DialogBottomSheetContent {
|
||||
registrarInEdit!: Registrar;
|
||||
private elementRef:
|
||||
| MatBottomSheetRef<RegistrarDetailsComponent>
|
||||
| MatDialogRef<RegistrarDetailsComponent>;
|
||||
params?: RegistrarDetailsParams;
|
||||
|
||||
constructor(
|
||||
protected registrarService: RegistrarService,
|
||||
private injector: Injector
|
||||
) {
|
||||
// We only inject one, either Dialog or Bottom Sheet data
|
||||
// so one of the injectors is expected to fail
|
||||
try {
|
||||
var params = this.injector.get(MAT_DIALOG_DATA);
|
||||
this.elementRef = this.injector.get(MatDialogRef);
|
||||
} catch (e) {
|
||||
var params = this.injector.get(MAT_BOTTOM_SHEET_DATA);
|
||||
this.elementRef = this.injector.get(MatBottomSheetRef);
|
||||
}
|
||||
this.registrarInEdit = JSON.parse(JSON.stringify(params.registrar));
|
||||
constructor(protected registrarService: RegistrarService) {}
|
||||
|
||||
init(params: RegistrarDetailsParams) {
|
||||
this.params = params;
|
||||
this.registrarInEdit = JSON.parse(
|
||||
JSON.stringify(this.params.data.registrar)
|
||||
);
|
||||
}
|
||||
|
||||
onCancel(e: MouseEvent) {
|
||||
if (this.elementRef instanceof MatBottomSheetRef) {
|
||||
this.elementRef.dismiss();
|
||||
} else if (this.elementRef instanceof MatDialogRef) {
|
||||
this.elementRef.close();
|
||||
}
|
||||
}
|
||||
|
||||
saveAndClose(e: MouseEvent) {
|
||||
// TODO: Implement save call to API
|
||||
this.onCancel(e);
|
||||
saveAndClose() {
|
||||
this.params?.close();
|
||||
}
|
||||
|
||||
addTLD(e: MatChipInputEvent) {
|
||||
@@ -82,24 +59,3 @@ export class RegistrarDetailsComponent {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar-details-wrapper',
|
||||
template: '',
|
||||
})
|
||||
export class RegistrarDetailsWrapperComponent {
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private bottomSheet: MatBottomSheet,
|
||||
protected breakpointObserver: BreakpointObserver
|
||||
) {}
|
||||
|
||||
open(registrar: Registrar) {
|
||||
const config = { data: { registrar } };
|
||||
if (this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT)) {
|
||||
this.bottomSheet.open(RegistrarDetailsComponent, config);
|
||||
} else {
|
||||
this.dialog.open(RegistrarDetailsComponent, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
[pageSizeOptions]="[5, 10, 20]"
|
||||
showFirstLastButtons
|
||||
></mat-paginator>
|
||||
<app-registrar-details-wrapper
|
||||
<app-dialog-bottom-sheet-wrapper
|
||||
#registrarDetailsView
|
||||
></app-registrar-details-wrapper>
|
||||
></app-dialog-bottom-sheet-wrapper>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,8 @@ import { Registrar, RegistrarService } from './registrar.service';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { RegistrarDetailsWrapperComponent } from './registrarDetails.component';
|
||||
import { RegistrarDetailsComponent } from './registrarDetails.component';
|
||||
import { DialogBottomSheetWrapper } from '../shared/components/dialogBottomSheet.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-registrar',
|
||||
@@ -82,7 +83,7 @@ export class RegistrarComponent {
|
||||
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
||||
@ViewChild(MatSort) sort!: MatSort;
|
||||
@ViewChild('registrarDetailsView')
|
||||
detailsComponentWrapper!: RegistrarDetailsWrapperComponent;
|
||||
detailsComponentWrapper!: DialogBottomSheetWrapper;
|
||||
|
||||
constructor(protected registrarService: RegistrarService) {
|
||||
this.dataSource = new MatTableDataSource<Registrar>(
|
||||
@@ -97,7 +98,10 @@ export class RegistrarComponent {
|
||||
|
||||
openDetails(event: MouseEvent, registrar: Registrar) {
|
||||
event.stopPropagation();
|
||||
this.detailsComponentWrapper.open(registrar);
|
||||
this.detailsComponentWrapper.open<RegistrarDetailsComponent>(
|
||||
RegistrarDetailsComponent,
|
||||
{ registrar }
|
||||
);
|
||||
}
|
||||
|
||||
applyFilter(event: Event) {
|
||||
|
||||
@@ -41,4 +41,7 @@
|
||||
<mat-icon>add</mat-icon>Create a Contact
|
||||
</button>
|
||||
</div>
|
||||
<app-dialog-bottom-sheet-wrapper
|
||||
#contactDetailsWrapper
|
||||
></app-dialog-bottom-sheet-wrapper>
|
||||
</div>
|
||||
|
||||
@@ -12,21 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import {
|
||||
MatDialog,
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogRef,
|
||||
} from '@angular/material/dialog';
|
||||
import {
|
||||
MatBottomSheet,
|
||||
MAT_BOTTOM_SHEET_DATA,
|
||||
MatBottomSheetRef,
|
||||
} from '@angular/material/bottom-sheet';
|
||||
import { Component, ViewChild } from '@angular/core';
|
||||
import { Contact, ContactService } from './contact.service';
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import {
|
||||
DialogBottomSheetContent,
|
||||
DialogBottomSheetWrapper,
|
||||
} from 'src/app/shared/components/dialogBottomSheet.component';
|
||||
|
||||
enum Operations {
|
||||
DELETE,
|
||||
@@ -40,7 +33,13 @@ interface GroupedContacts {
|
||||
contacts: Array<Contact>;
|
||||
}
|
||||
|
||||
let isMobile: boolean;
|
||||
type ContactDetailsParams = {
|
||||
close: Function;
|
||||
data: {
|
||||
contact: Contact;
|
||||
operation: Operations;
|
||||
};
|
||||
};
|
||||
|
||||
const contactTypes: Array<GroupedContacts> = [
|
||||
{ value: 'ADMIN', label: 'Primary contact', contacts: [] },
|
||||
@@ -52,72 +51,46 @@ const contactTypes: Array<GroupedContacts> = [
|
||||
{ value: 'WHOIS', label: 'WHOIS-Inquiry contact', contacts: [] },
|
||||
];
|
||||
|
||||
class ContactDetailsEventsResponder {
|
||||
private ref?: MatDialogRef<any> | MatBottomSheetRef;
|
||||
constructor() {
|
||||
this.onClose = this.onClose.bind(this);
|
||||
}
|
||||
|
||||
setRef(ref: MatDialogRef<any> | MatBottomSheetRef) {
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if (this.ref == undefined) {
|
||||
throw "Reference to ContactDetailsDialogComponent hasn't been set. ";
|
||||
}
|
||||
if (this.ref instanceof MatBottomSheetRef) {
|
||||
this.ref.dismiss();
|
||||
} else if (this.ref instanceof MatDialogRef) {
|
||||
this.ref.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-contact-details-dialog',
|
||||
templateUrl: 'contactDetails.component.html',
|
||||
styleUrls: ['./contact.component.scss'],
|
||||
})
|
||||
export class ContactDetailsDialogComponent {
|
||||
contact: Contact;
|
||||
export class ContactDetailsDialogComponent implements DialogBottomSheetContent {
|
||||
contact?: Contact;
|
||||
contactTypes = contactTypes;
|
||||
operation: Operations;
|
||||
contactIndex: number;
|
||||
onCloseCallback: Function;
|
||||
contactIndex?: number;
|
||||
|
||||
params?: ContactDetailsParams;
|
||||
|
||||
constructor(
|
||||
public contactService: ContactService,
|
||||
private _snackBar: MatSnackBar,
|
||||
@Inject(isMobile ? MAT_BOTTOM_SHEET_DATA : MAT_DIALOG_DATA)
|
||||
public data: {
|
||||
onClose: Function;
|
||||
contact: Contact;
|
||||
operation: Operations;
|
||||
}
|
||||
) {
|
||||
this.onCloseCallback = data.onClose;
|
||||
this.contactIndex = contactService.contacts.findIndex(
|
||||
(c) => c === data.contact
|
||||
private _snackBar: MatSnackBar
|
||||
) {}
|
||||
|
||||
init(params: ContactDetailsParams) {
|
||||
this.params = params;
|
||||
this.contactIndex = this.contactService.contacts.findIndex(
|
||||
(c) => c === params.data.contact
|
||||
);
|
||||
this.contact = JSON.parse(JSON.stringify(data.contact));
|
||||
this.operation = data.operation;
|
||||
this.contact = JSON.parse(JSON.stringify(params.data.contact));
|
||||
}
|
||||
|
||||
onClose(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
this.onCloseCallback.call(this);
|
||||
close() {
|
||||
this.params?.close();
|
||||
}
|
||||
|
||||
saveAndClose(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
if (!this.contact || this.contactIndex === undefined) return;
|
||||
if (!(e.target as HTMLFormElement).checkValidity()) {
|
||||
return;
|
||||
}
|
||||
const operation = this.params?.data.operation;
|
||||
let operationObservable;
|
||||
if (this.operation === Operations.ADD) {
|
||||
if (operation === Operations.ADD) {
|
||||
operationObservable = this.contactService.addContact(this.contact);
|
||||
} else if (this.operation === Operations.UPDATE) {
|
||||
} else if (operation === Operations.UPDATE) {
|
||||
operationObservable = this.contactService.updateContact(
|
||||
this.contactIndex,
|
||||
this.contact
|
||||
@@ -127,7 +100,7 @@ export class ContactDetailsDialogComponent {
|
||||
}
|
||||
|
||||
operationObservable.subscribe({
|
||||
complete: this.onCloseCallback.bind(this),
|
||||
complete: () => this.close(),
|
||||
error: (err: HttpErrorResponse) => {
|
||||
this._snackBar.open(err.error);
|
||||
},
|
||||
@@ -143,11 +116,11 @@ export class ContactDetailsDialogComponent {
|
||||
export default class ContactComponent {
|
||||
public static PATH = 'contact';
|
||||
|
||||
@ViewChild('contactDetailsWrapper')
|
||||
detailsComponentWrapper!: DialogBottomSheetWrapper;
|
||||
|
||||
loading: boolean = false;
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private bottomSheet: MatBottomSheet,
|
||||
private breakpointObserver: BreakpointObserver,
|
||||
public contactService: ContactService,
|
||||
private _snackBar: MatSnackBar
|
||||
) {
|
||||
@@ -195,20 +168,9 @@ export default class ContactComponent {
|
||||
operation: Operations = Operations.UPDATE
|
||||
) {
|
||||
e.preventDefault();
|
||||
// TODO: handle orientation change
|
||||
isMobile = this.breakpointObserver.isMatched('(max-width: 599px)');
|
||||
const responder = new ContactDetailsEventsResponder();
|
||||
const config = { data: { onClose: responder.onClose, contact, operation } };
|
||||
|
||||
if (isMobile) {
|
||||
const bottomSheetRef = this.bottomSheet.open(
|
||||
ContactDetailsDialogComponent,
|
||||
config
|
||||
);
|
||||
responder.setRef(bottomSheetRef);
|
||||
} else {
|
||||
const dialogRef = this.dialog.open(ContactDetailsDialogComponent, config);
|
||||
responder.setRef(dialogRef);
|
||||
}
|
||||
this.detailsComponentWrapper.open<ContactDetailsDialogComponent>(
|
||||
ContactDetailsDialogComponent,
|
||||
{ contact, operation }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export class ContactService {
|
||||
return this.backend
|
||||
.getContacts(this.registrarService.activeRegistrarId)
|
||||
.pipe(
|
||||
tap((contacts) => {
|
||||
tap((contacts = []) => {
|
||||
this.contacts = contacts;
|
||||
})
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<h3 mat-dialog-title>Contact details</h3>
|
||||
<div mat-dialog-content>
|
||||
<div mat-dialog-content *ngIf="contact">
|
||||
<form (ngSubmit)="saveAndClose($event)">
|
||||
<p>
|
||||
<mat-form-field class="contact-details__input">
|
||||
@@ -97,7 +97,7 @@
|
||||
>
|
||||
</section>
|
||||
<mat-dialog-actions>
|
||||
<button mat-button (click)="onClose($event)">Cancel</button>
|
||||
<button mat-button (click)="close()">Cancel</button>
|
||||
<button type="submit" mat-button>Save</button>
|
||||
</mat-dialog-actions>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Copyright 2023 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.
|
||||
|
||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||
import { ComponentType } from '@angular/cdk/portal';
|
||||
import { Component } from '@angular/core';
|
||||
import {
|
||||
MatBottomSheet,
|
||||
MatBottomSheetRef,
|
||||
} from '@angular/material/bottom-sheet';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
|
||||
const MOBILE_LAYOUT_BREAKPOINT = '(max-width: 599px)';
|
||||
|
||||
export interface DialogBottomSheetContent {
|
||||
init(data: Object): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps up a child component in an Angular Material Dalog for desktop or a Bottom Sheet
|
||||
* component for mobile depending on a screen resolution, with Breaking Point being 599px.
|
||||
* Child component is required to implement @see DialogBottomSheetContent interface
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-dialog-bottom-sheet-wrapper',
|
||||
template: '',
|
||||
})
|
||||
export class DialogBottomSheetWrapper {
|
||||
private elementRef?: MatBottomSheetRef | MatDialogRef<any>;
|
||||
|
||||
constructor(
|
||||
private dialog: MatDialog,
|
||||
private bottomSheet: MatBottomSheet,
|
||||
protected breakpointObserver: BreakpointObserver
|
||||
) {}
|
||||
|
||||
open<T extends DialogBottomSheetContent>(
|
||||
component: ComponentType<T>,
|
||||
data: any
|
||||
) {
|
||||
const config = { data, close: () => this.close() };
|
||||
if (this.breakpointObserver.isMatched(MOBILE_LAYOUT_BREAKPOINT)) {
|
||||
this.elementRef = this.bottomSheet.open(component);
|
||||
this.elementRef.instance.init(config);
|
||||
} else {
|
||||
this.elementRef = this.dialog.open(component);
|
||||
this.elementRef.componentInstance.init(config);
|
||||
}
|
||||
}
|
||||
|
||||
close() {
|
||||
if (this.elementRef instanceof MatBottomSheetRef) {
|
||||
this.elementRef.dismiss();
|
||||
} else if (this.elementRef instanceof MatDialogRef) {
|
||||
this.elementRef.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,8 @@ export class BackendService {
|
||||
checkpointTime?: string,
|
||||
pageNumber?: number,
|
||||
resultsPerPage?: number,
|
||||
totalResults?: number
|
||||
totalResults?: number,
|
||||
searchTerm?: string
|
||||
): Observable<DomainListResult> {
|
||||
var url = `/console-api/domain-list?registrarId=${registrarId}`;
|
||||
if (checkpointTime) {
|
||||
@@ -84,6 +85,9 @@ export class BackendService {
|
||||
if (totalResults) {
|
||||
url += `&totalResults=${totalResults}`;
|
||||
}
|
||||
if (searchTerm) {
|
||||
url += `&searchTerm=${searchTerm}`;
|
||||
}
|
||||
return this.http
|
||||
.get<DomainListResult>(url)
|
||||
.pipe(catchError((err) => this.errorCatcher<DomainListResult>(err)));
|
||||
|
||||
@@ -17,15 +17,14 @@ package google.registry.batch;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.batch.BatchModule.PARAM_DRY_RUN;
|
||||
import static google.registry.config.RegistryEnvironment.PRODUCTION;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.request.Action.Method.POST;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.RegistryEnvironment.PRODUCTION;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.flows.poll.PollFlowUtils;
|
||||
import google.registry.model.EppResource;
|
||||
import google.registry.model.EppResourceUtils;
|
||||
@@ -40,6 +39,7 @@ import google.registry.request.Action;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,13 +18,13 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.batch.BatchModule.PARAM_DRY_RUN;
|
||||
import static google.registry.config.RegistryEnvironment.PRODUCTION;
|
||||
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_DELETE;
|
||||
import static google.registry.model.tld.Tlds.getTldsOfType;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.request.Action.Method.POST;
|
||||
import static google.registry.request.RequestParameters.PARAM_TLDS;
|
||||
import static google.registry.util.RegistryEnvironment.PRODUCTION;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@@ -32,7 +32,6 @@ import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.CreateAutoTimestamp;
|
||||
import google.registry.model.EppResourceUtils;
|
||||
import google.registry.model.domain.Domain;
|
||||
@@ -41,6 +40,7 @@ import google.registry.model.tld.Tld.TldType;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import javax.inject.Inject;
|
||||
import org.hibernate.CacheMode;
|
||||
|
||||
@@ -31,7 +31,6 @@ import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.beam.billing.ExpandBillingRecurrencesPipeline;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.billing.BillingEvent;
|
||||
import google.registry.model.billing.BillingRecurrence;
|
||||
import google.registry.model.common.Cursor;
|
||||
@@ -40,6 +39,7 @@ import google.registry.request.Parameter;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -27,12 +27,12 @@ import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,7 +28,6 @@ import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.beam.wipeout.WipeOutContactHistoryPiiPipeline;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.contact.ContactHistory;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Action.Service;
|
||||
@@ -36,6 +35,7 @@ import google.registry.request.Parameter;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
|
||||
package google.registry.beam.common;
|
||||
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.persistence.PersistenceModule.JpaTransactionManagerType;
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.util.Objects;
|
||||
import javax.annotation.Nullable;
|
||||
import org.apache.beam.sdk.extensions.gcp.options.GcpOptions;
|
||||
|
||||
@@ -19,10 +19,10 @@ import static google.registry.beam.common.RegistryPipelineOptions.toRegistryPipe
|
||||
import com.google.auto.service.AutoService;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import dagger.Lazy;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.config.SystemPropertySetter;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
import google.registry.persistence.transaction.TransactionManagerFactory;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import google.registry.util.SystemPropertySetter;
|
||||
import org.apache.beam.sdk.harness.JvmInitializer;
|
||||
import org.apache.beam.sdk.options.PipelineOptions;
|
||||
|
||||
|
||||
164
core/src/main/java/google/registry/bsa/BlockListFetcher.java
Normal file
164
core/src/main/java/google/registry/bsa/BlockListFetcher.java
Normal file
@@ -0,0 +1,164 @@
|
||||
// Copyright 2023 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.bsa;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import com.google.api.client.http.HttpMethods;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import google.registry.bsa.api.BsaCredential;
|
||||
import google.registry.bsa.api.BsaException;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.request.UrlConnectionService;
|
||||
import google.registry.util.Retrier;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.function.BiConsumer;
|
||||
import javax.inject.Inject;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
||||
/** Fetches data from the BSA API. */
|
||||
public class BlockListFetcher {
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private final UrlConnectionService urlConnectionService;
|
||||
private final BsaCredential credential;
|
||||
|
||||
private final ImmutableMap<String, String> blockListUrls;
|
||||
private final Retrier retrier;
|
||||
|
||||
@Inject
|
||||
BlockListFetcher(
|
||||
UrlConnectionService urlConnectionService,
|
||||
BsaCredential credential,
|
||||
@Config("bsaDataUrls") ImmutableMap<String, String> blockListUrls,
|
||||
Retrier retrier) {
|
||||
this.urlConnectionService = urlConnectionService;
|
||||
this.credential = credential;
|
||||
this.blockListUrls = blockListUrls;
|
||||
this.retrier = retrier;
|
||||
}
|
||||
|
||||
LazyBlockList fetch(BlockListType blockListType) {
|
||||
// TODO: use more informative exceptions to describe retriable errors
|
||||
return retrier.callWithRetry(
|
||||
() -> tryFetch(blockListType),
|
||||
e -> e instanceof BsaException && ((BsaException) e).isRetriable());
|
||||
}
|
||||
|
||||
LazyBlockList tryFetch(BlockListType blockListType) {
|
||||
try {
|
||||
URL dataUrl = new URL(blockListUrls.get(blockListType.name()));
|
||||
logger.atInfo().log("Downloading from %s", dataUrl);
|
||||
HttpsURLConnection connection =
|
||||
(HttpsURLConnection) urlConnectionService.createConnection(dataUrl);
|
||||
connection.setRequestMethod(HttpMethods.GET);
|
||||
connection.setRequestProperty("Authorization", "Bearer " + credential.getAuthToken());
|
||||
int code = connection.getResponseCode();
|
||||
if (code != SC_OK) {
|
||||
String errorDetails = "";
|
||||
try (InputStream errorStream = connection.getErrorStream()) {
|
||||
errorDetails = new String(ByteStreams.toByteArray(errorStream), UTF_8);
|
||||
} catch (NullPointerException e) {
|
||||
// No error message.
|
||||
} catch (Exception e) {
|
||||
errorDetails = "Failed to retrieve error message: " + e.getMessage();
|
||||
}
|
||||
throw new BsaException(
|
||||
String.format(
|
||||
"Status code: [%s], error: [%s], details: [%s]",
|
||||
code, connection.getResponseMessage(), errorDetails),
|
||||
/* retriable= */ true);
|
||||
}
|
||||
return new LazyBlockList(blockListType, connection);
|
||||
} catch (IOException e) {
|
||||
throw new BsaException(e, /* retriable= */ true);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new BsaException(e, /* retriable= */ false);
|
||||
}
|
||||
}
|
||||
|
||||
static class LazyBlockList implements Closeable {
|
||||
|
||||
private final BlockListType blockListType;
|
||||
|
||||
private final HttpsURLConnection connection;
|
||||
|
||||
private final BufferedInputStream inputStream;
|
||||
private final String checksum;
|
||||
|
||||
LazyBlockList(BlockListType blockListType, HttpsURLConnection connection) throws IOException {
|
||||
this.blockListType = blockListType;
|
||||
this.connection = connection;
|
||||
this.inputStream = new BufferedInputStream(connection.getInputStream());
|
||||
this.checksum = readChecksum();
|
||||
}
|
||||
|
||||
/** Reads the BSA-generated checksum, which is the first line of the input. */
|
||||
private String readChecksum() throws IOException {
|
||||
StringBuilder checksum = new StringBuilder();
|
||||
char ch;
|
||||
while ((ch = peekInputStream()) != (char) -1 && !Character.isWhitespace(ch)) {
|
||||
checksum.append((char) inputStream.read());
|
||||
}
|
||||
while ((ch = peekInputStream()) != (char) -1 && Character.isWhitespace(ch)) {
|
||||
inputStream.read();
|
||||
}
|
||||
return checksum.toString();
|
||||
}
|
||||
|
||||
char peekInputStream() throws IOException {
|
||||
inputStream.mark(1);
|
||||
int byteValue = inputStream.read();
|
||||
inputStream.reset();
|
||||
return (char) byteValue;
|
||||
}
|
||||
|
||||
BlockListType getName() {
|
||||
return blockListType;
|
||||
}
|
||||
|
||||
String checksum() {
|
||||
return checksum;
|
||||
}
|
||||
|
||||
void consumeAll(BiConsumer<byte[], Integer> consumer) throws IOException {
|
||||
byte[] buffer = new byte[1024];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
consumer.accept(buffer, bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
// Fall through to close the connection.
|
||||
}
|
||||
}
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,10 @@
|
||||
|
||||
package google.registry.bsa;
|
||||
|
||||
/** Identifiers of the BSA lists with blocking labels. */
|
||||
public enum BlockList {
|
||||
/**
|
||||
* The product types of the block lists, which determines the http endpoint that serves the data.
|
||||
*/
|
||||
public enum BlockListType {
|
||||
BLOCK,
|
||||
BLOCK_PLUS;
|
||||
}
|
||||
267
core/src/main/java/google/registry/bsa/BsaDiffCreator.java
Normal file
267
core/src/main/java/google/registry/bsa/BsaDiffCreator.java
Normal file
@@ -0,0 +1,267 @@
|
||||
// Copyright 2023 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.bsa;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.Maps.newHashMap;
|
||||
import static com.google.common.collect.Multimaps.newListMultimap;
|
||||
import static com.google.common.collect.Multimaps.toMultimap;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
import com.google.common.collect.Sets;
|
||||
import google.registry.bsa.api.BlockLabel;
|
||||
import google.registry.bsa.api.BlockLabel.LabelType;
|
||||
import google.registry.bsa.api.BlockOrder;
|
||||
import google.registry.bsa.api.BlockOrder.OrderType;
|
||||
import google.registry.bsa.persistence.DownloadSchedule;
|
||||
import google.registry.bsa.persistence.DownloadSchedule.CompletedJob;
|
||||
import google.registry.tldconfig.idn.IdnTableEnum;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/** Creates diffs between the most recent download and the previous one. */
|
||||
class BsaDiffCreator {
|
||||
|
||||
private static final Splitter LINE_SPLITTER = Splitter.on(',').trimResults();
|
||||
private static final Splitter ORDER_SPLITTER = Splitter.on(';').trimResults();
|
||||
|
||||
private static final String BSA_CSV_HEADER = "domainLabel,orderIDs";
|
||||
|
||||
/** An impossible value for order ID. See {@link #createDiff} for usage. */
|
||||
static final Long ORDER_ID_SENTINEL = Long.MIN_VALUE;
|
||||
|
||||
private final GcsClient gcsClient;
|
||||
|
||||
@Inject
|
||||
BsaDiffCreator(GcsClient gcsClient) {
|
||||
this.gcsClient = gcsClient;
|
||||
}
|
||||
|
||||
private <K, V extends Comparable> Multimap<K, V> listBackedMultiMap() {
|
||||
return newListMultimap(newHashMap(), Lists::newArrayList);
|
||||
}
|
||||
|
||||
BsaDiff createDiff(DownloadSchedule schedule, IdnChecker idnChecker) {
|
||||
String currentJobName = schedule.jobName();
|
||||
Optional<String> previousJobName = schedule.latestCompleted().map(CompletedJob::jobName);
|
||||
/**
|
||||
* Memory usage is a concern when creating a diff, when the newest download needs to be held in
|
||||
* memory in its entirety. The top-grade AppEngine VM has 3GB of memory, leaving less than 1.5GB
|
||||
* to application memory footprint after subtracting overheads due to copying garbage collection
|
||||
* and non-heap data etc. Assuming 400K labels, each of which on average included in 5 orders,
|
||||
* the memory footprint is at least 300MB when loaded into a Hashset-backed Multimap (64-bit
|
||||
* JVM, with 12-byte object header, 16-byte array header, and 16-byte alignment).
|
||||
*
|
||||
* <p>The memory footprint can be reduced in two ways, by using a canonical instance for each
|
||||
* order ID value, and by using a ArrayList-backed Multimap. Together they reduce memory size to
|
||||
* well below 100MB for the scenario above.
|
||||
*
|
||||
* <p>We need to watch out for the download sizes even after the migration to GKE. However, at
|
||||
* that point we will have a wider selection of hardware.
|
||||
*
|
||||
* <p>Beam pipeline is not a good option. It has to be launched as a separate, asynchronous job,
|
||||
* and there is no guaranteed limit to launch delay. Both issues would increase code complexity.
|
||||
*/
|
||||
Canonicals<Long> canonicals = new Canonicals<>();
|
||||
try (Stream<Line> currentStream = loadBlockLists(currentJobName);
|
||||
Stream<Line> previousStream =
|
||||
previousJobName.map(this::loadBlockLists).orElseGet(Stream::of)) {
|
||||
/**
|
||||
* Load current label/order pairs into a multimap, which will contain both new labels and
|
||||
* those that stay on when processing is done.
|
||||
*/
|
||||
Multimap<String, Long> newAndRemaining =
|
||||
currentStream
|
||||
.map(line -> line.labelOrderPairs(canonicals))
|
||||
.flatMap(x -> x)
|
||||
.collect(
|
||||
toMultimap(
|
||||
LabelOrderPair::label, LabelOrderPair::orderId, this::listBackedMultiMap));
|
||||
|
||||
Multimap<String, Long> deleted =
|
||||
previousStream
|
||||
.map(
|
||||
line -> {
|
||||
// Mark labels that exist in both downloads with the SENTINEL id. This helps
|
||||
// distinguish existing label with new order from new labels.
|
||||
if (newAndRemaining.containsKey(line.label())
|
||||
&& !newAndRemaining.containsEntry(line.label(), ORDER_ID_SENTINEL)) {
|
||||
newAndRemaining.put(line.label(), ORDER_ID_SENTINEL);
|
||||
}
|
||||
return line;
|
||||
})
|
||||
.map(line -> line.labelOrderPairs(canonicals))
|
||||
.flatMap(x -> x)
|
||||
.filter(kv -> !newAndRemaining.remove(kv.label(), kv.orderId()))
|
||||
.collect(
|
||||
toMultimap(
|
||||
LabelOrderPair::label, LabelOrderPair::orderId, this::listBackedMultiMap));
|
||||
|
||||
/**
|
||||
* Labels in `newAndRemaining`:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Mapped to `sentinel` only: Labels without change, ignore
|
||||
* <li>Mapped to `sentinel` and some orders: Existing labels with new order mapping. Those
|
||||
* orders are new orders.
|
||||
* <li>Mapped to some orders but not `sentinel`: New labels and new orders.
|
||||
* </ul>
|
||||
*
|
||||
* <p>The `deleted` map has
|
||||
*
|
||||
* <ul>
|
||||
* <li>Deleted labels: the keyset of deleted minus the keyset of the newAndRemaining
|
||||
* <li>Deleted orders: the union of values.
|
||||
* </ul>
|
||||
*/
|
||||
return new BsaDiff(
|
||||
ImmutableMultimap.copyOf(newAndRemaining), ImmutableMultimap.copyOf(deleted), idnChecker);
|
||||
}
|
||||
}
|
||||
|
||||
Stream<Line> loadBlockLists(String jobName) {
|
||||
return Stream.of(BlockListType.values())
|
||||
.map(blockList -> gcsClient.readBlockList(jobName, blockList))
|
||||
.flatMap(x -> x)
|
||||
.filter(line -> !line.startsWith(BSA_CSV_HEADER))
|
||||
.map(BsaDiffCreator::parseLine);
|
||||
}
|
||||
|
||||
static Line parseLine(String line) {
|
||||
List<String> columns = LINE_SPLITTER.splitToList(line);
|
||||
checkArgument(columns.size() == 2, "Invalid line: [%s]", line);
|
||||
checkArgument(!Strings.isNullOrEmpty(columns.get(0)), "Missing label in line: [%s]", line);
|
||||
try {
|
||||
ImmutableList<Long> orderIds =
|
||||
ORDER_SPLITTER
|
||||
.splitToStream(columns.get(1))
|
||||
.map(Long::valueOf)
|
||||
.collect(toImmutableList());
|
||||
checkArgument(!orderIds.isEmpty(), "Missing orders in line: [%s]", line);
|
||||
checkArgument(
|
||||
!orderIds.contains(ORDER_ID_SENTINEL), "Invalid order id %s", ORDER_ID_SENTINEL);
|
||||
return Line.of(columns.get(0), orderIds);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException(line, e);
|
||||
}
|
||||
}
|
||||
|
||||
static class BsaDiff {
|
||||
private final ImmutableMultimap<String, Long> newAndRemaining;
|
||||
|
||||
private final ImmutableMultimap<String, Long> deleted;
|
||||
private final IdnChecker idnChecker;
|
||||
|
||||
BsaDiff(
|
||||
ImmutableMultimap<String, Long> newAndRemaining,
|
||||
ImmutableMultimap<String, Long> deleted,
|
||||
IdnChecker idnChecker) {
|
||||
this.newAndRemaining = newAndRemaining;
|
||||
this.deleted = deleted;
|
||||
this.idnChecker = idnChecker;
|
||||
}
|
||||
|
||||
Stream<BlockOrder> getOrders() {
|
||||
return Stream.concat(
|
||||
newAndRemaining.values().stream()
|
||||
.filter(value -> !Objects.equals(ORDER_ID_SENTINEL, value))
|
||||
.distinct()
|
||||
.map(id -> BlockOrder.of(id, OrderType.CREATE)),
|
||||
deleted.values().stream().distinct().map(id -> BlockOrder.of(id, OrderType.DELETE)));
|
||||
}
|
||||
|
||||
Stream<BlockLabel> getLabels() {
|
||||
return Stream.of(
|
||||
newAndRemaining.asMap().entrySet().stream()
|
||||
.filter(e -> e.getValue().size() > 1 || !e.getValue().contains(ORDER_ID_SENTINEL))
|
||||
.filter(entry -> entry.getValue().contains(ORDER_ID_SENTINEL))
|
||||
.map(
|
||||
entry ->
|
||||
BlockLabel.of(
|
||||
entry.getKey(),
|
||||
LabelType.NEW_ORDER_ASSOCIATION,
|
||||
idnChecker.getAllValidIdns(entry.getKey()).stream()
|
||||
.map(IdnTableEnum::name)
|
||||
.collect(toImmutableSet()))),
|
||||
newAndRemaining.asMap().entrySet().stream()
|
||||
.filter(e -> e.getValue().size() > 1 || !e.getValue().contains(ORDER_ID_SENTINEL))
|
||||
.filter(entry -> !entry.getValue().contains(ORDER_ID_SENTINEL))
|
||||
.map(
|
||||
entry ->
|
||||
BlockLabel.of(
|
||||
entry.getKey(),
|
||||
LabelType.CREATE,
|
||||
idnChecker.getAllValidIdns(entry.getKey()).stream()
|
||||
.map(IdnTableEnum::name)
|
||||
.collect(toImmutableSet()))),
|
||||
Sets.difference(deleted.keySet(), newAndRemaining.keySet()).stream()
|
||||
.map(label -> BlockLabel.of(label, LabelType.DELETE, ImmutableSet.of())))
|
||||
.flatMap(x -> x);
|
||||
}
|
||||
}
|
||||
|
||||
static class Canonicals<T> {
|
||||
private final HashMap<T, T> cache;
|
||||
|
||||
Canonicals() {
|
||||
cache = Maps.newHashMap();
|
||||
}
|
||||
|
||||
T get(T value) {
|
||||
cache.putIfAbsent(value, value);
|
||||
return cache.get(value);
|
||||
}
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class LabelOrderPair {
|
||||
abstract String label();
|
||||
|
||||
abstract Long orderId();
|
||||
|
||||
static <K, V> LabelOrderPair of(String key, Long value) {
|
||||
return new AutoValue_BsaDiffCreator_LabelOrderPair(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class Line {
|
||||
abstract String label();
|
||||
|
||||
abstract ImmutableList<Long> orderIds();
|
||||
|
||||
Stream<LabelOrderPair> labelOrderPairs(Canonicals<Long> canonicals) {
|
||||
return orderIds().stream().map(id -> LabelOrderPair.of(label(), canonicals.get(id)));
|
||||
}
|
||||
|
||||
static Line of(String label, ImmutableList<Long> orderIds) {
|
||||
return new AutoValue_BsaDiffCreator_Line(label, orderIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
237
core/src/main/java/google/registry/bsa/BsaDownloadAction.java
Normal file
237
core/src/main/java/google/registry/bsa/BsaDownloadAction.java
Normal file
@@ -0,0 +1,237 @@
|
||||
// Copyright 2023 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.bsa;
|
||||
|
||||
import static google.registry.bsa.BlockListType.BLOCK;
|
||||
import static google.registry.bsa.BlockListType.BLOCK_PLUS;
|
||||
import static google.registry.bsa.api.JsonSerializations.toCompletedOrdersReport;
|
||||
import static google.registry.bsa.api.JsonSerializations.toInProgressOrdersReport;
|
||||
import static google.registry.bsa.api.JsonSerializations.toUnblockableDomainsReport;
|
||||
import static google.registry.bsa.persistence.LabelDiffUpdates.applyLabelDiff;
|
||||
import static google.registry.request.Action.Method.GET;
|
||||
import static google.registry.request.Action.Method.POST;
|
||||
import static google.registry.util.BatchedStreams.toBatches;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import dagger.Lazy;
|
||||
import google.registry.bsa.BlockListFetcher.LazyBlockList;
|
||||
import google.registry.bsa.BsaDiffCreator.BsaDiff;
|
||||
import google.registry.bsa.api.BlockLabel;
|
||||
import google.registry.bsa.api.BlockOrder;
|
||||
import google.registry.bsa.api.BsaReportSender;
|
||||
import google.registry.bsa.api.UnblockableDomain;
|
||||
import google.registry.bsa.persistence.DownloadSchedule;
|
||||
import google.registry.bsa.persistence.DownloadScheduler;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.model.tld.Tlds;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Clock;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import javax.inject.Inject;
|
||||
|
||||
@Action(
|
||||
service = Action.Service.BSA,
|
||||
path = BsaDownloadAction.PATH,
|
||||
method = {GET, POST},
|
||||
auth = Auth.AUTH_API_ADMIN)
|
||||
public class BsaDownloadAction implements Runnable {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
static final String PATH = "/_dr/task/bsaDownload";
|
||||
|
||||
private final DownloadScheduler downloadScheduler;
|
||||
private final BlockListFetcher blockListFetcher;
|
||||
private final BsaDiffCreator diffCreator;
|
||||
private final BsaReportSender bsaReportSender;
|
||||
private final GcsClient gcsClient;
|
||||
private final Lazy<IdnChecker> lazyIdnChecker;
|
||||
private final BsaLock bsaLock;
|
||||
private final Clock clock;
|
||||
private final int transactionBatchSize;
|
||||
private final Response response;
|
||||
|
||||
@Inject
|
||||
BsaDownloadAction(
|
||||
DownloadScheduler downloadScheduler,
|
||||
BlockListFetcher blockListFetcher,
|
||||
BsaDiffCreator diffCreator,
|
||||
BsaReportSender bsaReportSender,
|
||||
GcsClient gcsClient,
|
||||
Lazy<IdnChecker> lazyIdnChecker,
|
||||
BsaLock bsaLock,
|
||||
Clock clock,
|
||||
@Config("bsaTxnBatchSize") int transactionBatchSize,
|
||||
Response response) {
|
||||
this.downloadScheduler = downloadScheduler;
|
||||
this.blockListFetcher = blockListFetcher;
|
||||
this.diffCreator = diffCreator;
|
||||
this.bsaReportSender = bsaReportSender;
|
||||
this.gcsClient = gcsClient;
|
||||
this.lazyIdnChecker = lazyIdnChecker;
|
||||
this.bsaLock = bsaLock;
|
||||
this.clock = clock;
|
||||
this.transactionBatchSize = transactionBatchSize;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (!bsaLock.executeWithLock(this::runWithinLock)) {
|
||||
logger.atInfo().log("Job is being executed by another worker.");
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
// TODO(12/31/2023): consider sending an alert email.
|
||||
// TODO: if unretriable errors, log at severe and send email.
|
||||
logger.atWarning().withCause(throwable).log("Failed to update block lists.");
|
||||
}
|
||||
// Always return OK. Let the next cron job retry.
|
||||
response.setStatus(SC_OK);
|
||||
}
|
||||
|
||||
Void runWithinLock() {
|
||||
// Cannot enroll new TLDs after download starts. This may change if b/309175410 is fixed.
|
||||
if (!Tlds.hasActiveBsaEnrollment(clock.nowUtc())) {
|
||||
logger.atInfo().log("No TLDs enrolled with BSA. Quitting.");
|
||||
return null;
|
||||
}
|
||||
Optional<DownloadSchedule> scheduleOptional = downloadScheduler.schedule();
|
||||
if (!scheduleOptional.isPresent()) {
|
||||
logger.atInfo().log("Nothing to do.");
|
||||
return null;
|
||||
}
|
||||
BsaDiff diff = null;
|
||||
DownloadSchedule schedule = scheduleOptional.get();
|
||||
switch (schedule.stage()) {
|
||||
case DOWNLOAD_BLOCK_LISTS:
|
||||
try (LazyBlockList block = blockListFetcher.fetch(BLOCK);
|
||||
LazyBlockList blockPlus = blockListFetcher.fetch(BLOCK_PLUS)) {
|
||||
ImmutableMap<BlockListType, String> fetchedChecksums =
|
||||
ImmutableMap.of(BLOCK, block.checksum(), BLOCK_PLUS, blockPlus.checksum());
|
||||
ImmutableMap<BlockListType, String> prevChecksums =
|
||||
schedule
|
||||
.latestCompleted()
|
||||
.map(DownloadSchedule.CompletedJob::checksums)
|
||||
.orElseGet(ImmutableMap::of);
|
||||
boolean checksumsMatch = Objects.equals(fetchedChecksums, prevChecksums);
|
||||
if (!schedule.alwaysDownload() && checksumsMatch) {
|
||||
logger.atInfo().log(
|
||||
"Skipping download b/c block list checksums have not changed: [%s]",
|
||||
fetchedChecksums);
|
||||
schedule.updateJobStage(DownloadStage.NOP, fetchedChecksums);
|
||||
return null;
|
||||
} else if (checksumsMatch) {
|
||||
logger.atInfo().log(
|
||||
"Checksums match but download anyway: elapsed time since last download exceeds"
|
||||
+ " configured limit.");
|
||||
}
|
||||
// When downloading, always fetch both lists so that whole data set is in one GCS folder.
|
||||
ImmutableMap<BlockListType, String> actualChecksum =
|
||||
gcsClient.saveAndChecksumBlockList(
|
||||
schedule.jobName(), ImmutableList.of(block, blockPlus));
|
||||
if (!Objects.equals(fetchedChecksums, actualChecksum)) {
|
||||
logger.atSevere().log(
|
||||
"Inlined checksums do not match those calculated by us. Theirs: [%s]; ours: [%s]",
|
||||
fetchedChecksums, actualChecksum);
|
||||
schedule.updateJobStage(DownloadStage.CHECKSUMS_DO_NOT_MATCH, fetchedChecksums);
|
||||
// TODO(01/15/24): add email alert.
|
||||
return null;
|
||||
}
|
||||
schedule.updateJobStage(DownloadStage.MAKE_ORDER_AND_LABEL_DIFF, actualChecksum);
|
||||
}
|
||||
// Fall through
|
||||
case MAKE_ORDER_AND_LABEL_DIFF:
|
||||
diff = diffCreator.createDiff(schedule, lazyIdnChecker.get());
|
||||
gcsClient.writeOrderDiffs(schedule.jobName(), diff.getOrders());
|
||||
gcsClient.writeLabelDiffs(schedule.jobName(), diff.getLabels());
|
||||
schedule.updateJobStage(DownloadStage.APPLY_ORDER_AND_LABEL_DIFF);
|
||||
// Fall through
|
||||
case APPLY_ORDER_AND_LABEL_DIFF:
|
||||
try (Stream<BlockLabel> labels =
|
||||
diff != null ? diff.getLabels() : gcsClient.readLabelDiffs(schedule.jobName())) {
|
||||
Stream<ImmutableList<BlockLabel>> batches = toBatches(labels, transactionBatchSize);
|
||||
gcsClient.writeUnblockableDomains(
|
||||
schedule.jobName(),
|
||||
batches
|
||||
.map(
|
||||
batch ->
|
||||
applyLabelDiff(batch, lazyIdnChecker.get(), schedule, clock.nowUtc()))
|
||||
.flatMap(ImmutableList::stream));
|
||||
}
|
||||
schedule.updateJobStage(DownloadStage.REPORT_START_OF_ORDER_PROCESSING);
|
||||
// Fall through
|
||||
case REPORT_START_OF_ORDER_PROCESSING:
|
||||
try (Stream<BlockOrder> orders = gcsClient.readOrderDiffs(schedule.jobName())) {
|
||||
// We expect that all order instances and the json string can fit in memory.
|
||||
Optional<String> report = toInProgressOrdersReport(orders);
|
||||
if (report.isPresent()) {
|
||||
// Log report data
|
||||
gcsClient.logInProgressOrderReport(
|
||||
schedule.jobName(), BsaStringUtils.LINE_SPLITTER.splitToStream(report.get()));
|
||||
bsaReportSender.sendOrderStatusReport(report.get());
|
||||
} else {
|
||||
logger.atInfo().log("No new or deleted orders in this round.");
|
||||
}
|
||||
}
|
||||
schedule.updateJobStage(DownloadStage.UPLOAD_UNBLOCKABLE_DOMAINS_FOR_NEW_ORDERS);
|
||||
// Fall through
|
||||
case UPLOAD_UNBLOCKABLE_DOMAINS_FOR_NEW_ORDERS:
|
||||
try (Stream<UnblockableDomain> unblockables =
|
||||
gcsClient.readUnblockableDomains(schedule.jobName())) {
|
||||
/* The number of unblockable domains may be huge in theory (label x ~50 tlds), but in
|
||||
* practice should be relatively small (tens of thousands?). Batches can be introduced
|
||||
* if size becomes a problem.
|
||||
*/
|
||||
Optional<String> report = toUnblockableDomainsReport(unblockables);
|
||||
if (report.isPresent()) {
|
||||
gcsClient.logAddedUnblockableDomainsReport(
|
||||
schedule.jobName(), BsaStringUtils.LINE_SPLITTER.splitToStream(report.get()));
|
||||
// During downloads, unblockable domains are only added, not removed.
|
||||
bsaReportSender.addUnblockableDomainsUpdates(report.get());
|
||||
} else {
|
||||
logger.atInfo().log("No changes in the set of unblockable domains in this round.");
|
||||
}
|
||||
}
|
||||
schedule.updateJobStage(DownloadStage.REPORT_END_OF_ORDER_PROCESSING);
|
||||
// Fall through
|
||||
case REPORT_END_OF_ORDER_PROCESSING:
|
||||
try (Stream<BlockOrder> orders = gcsClient.readOrderDiffs(schedule.jobName())) {
|
||||
// Orders are expected to be few, so the report can be kept in memory.
|
||||
Optional<String> report = toCompletedOrdersReport(orders);
|
||||
if (report.isPresent()) {
|
||||
gcsClient.logCompletedOrderReport(
|
||||
schedule.jobName(), BsaStringUtils.LINE_SPLITTER.splitToStream(report.get()));
|
||||
bsaReportSender.sendOrderStatusReport(report.get());
|
||||
}
|
||||
}
|
||||
schedule.updateJobStage(DownloadStage.DONE);
|
||||
return null;
|
||||
case DONE:
|
||||
case NOP:
|
||||
case CHECKSUMS_DO_NOT_MATCH:
|
||||
logger.atWarning().log("Unexpectedly reached the %s stage.", schedule.stage());
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -14,32 +14,27 @@
|
||||
|
||||
package google.registry.bsa;
|
||||
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Action.Service;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.request.lock.LockHandler;
|
||||
import java.util.concurrent.Callable;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
@Action(
|
||||
service = Service.BSA,
|
||||
path = PlaceholderAction.PATH,
|
||||
method = Action.Method.GET,
|
||||
auth = Auth.AUTH_API_ADMIN)
|
||||
public class PlaceholderAction implements Runnable {
|
||||
private final Response response;
|
||||
/** Helper for guarding all BSA related work with a common lock. */
|
||||
public class BsaLock {
|
||||
|
||||
static final String PATH = "/_dr/task/bsaDownload";
|
||||
private static final String LOCK_NAME = "all-bsa-jobs";
|
||||
|
||||
private final LockHandler lockHandler;
|
||||
private final Duration leaseExpiry;
|
||||
|
||||
@Inject
|
||||
public PlaceholderAction(Response response) {
|
||||
this.response = response;
|
||||
BsaLock(LockHandler lockHandler, @Config("bsaLockLeaseExpiry") Duration leaseExpiry) {
|
||||
this.lockHandler = lockHandler;
|
||||
this.leaseExpiry = leaseExpiry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
response.setStatus(SC_OK);
|
||||
response.setPayload("Hello World");
|
||||
boolean executeWithLock(Callable<Void> callable) {
|
||||
return lockHandler.executeWithLocks(callable, null, leaseExpiry, LOCK_NAME);
|
||||
}
|
||||
}
|
||||
174
core/src/main/java/google/registry/bsa/BsaRefreshAction.java
Normal file
174
core/src/main/java/google/registry/bsa/BsaRefreshAction.java
Normal file
@@ -0,0 +1,174 @@
|
||||
// Copyright 2023 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.bsa;
|
||||
|
||||
import static google.registry.bsa.BsaStringUtils.LINE_SPLITTER;
|
||||
import static google.registry.request.Action.Method.GET;
|
||||
import static google.registry.request.Action.Method.POST;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.bsa.api.BsaReportSender;
|
||||
import google.registry.bsa.api.JsonSerializations;
|
||||
import google.registry.bsa.api.UnblockableDomainChange;
|
||||
import google.registry.bsa.persistence.DomainsRefresher;
|
||||
import google.registry.bsa.persistence.RefreshSchedule;
|
||||
import google.registry.bsa.persistence.RefreshScheduler;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.model.tld.Tlds;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.BatchedStreams;
|
||||
import google.registry.util.Clock;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
@Action(
|
||||
service = Action.Service.BSA,
|
||||
path = BsaRefreshAction.PATH,
|
||||
method = {GET, POST},
|
||||
auth = Auth.AUTH_API_ADMIN)
|
||||
public class BsaRefreshAction implements Runnable {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
static final String PATH = "/_dr/task/bsaRefresh";
|
||||
|
||||
private final RefreshScheduler scheduler;
|
||||
private final GcsClient gcsClient;
|
||||
private final BsaReportSender bsaReportSender;
|
||||
private final int transactionBatchSize;
|
||||
private final Duration domainTxnMaxDuration;
|
||||
private final BsaLock bsaLock;
|
||||
private final Clock clock;
|
||||
private final Response response;
|
||||
|
||||
@Inject
|
||||
BsaRefreshAction(
|
||||
RefreshScheduler scheduler,
|
||||
GcsClient gcsClient,
|
||||
BsaReportSender bsaReportSender,
|
||||
@Config("bsaTxnBatchSize") int transactionBatchSize,
|
||||
@Config("domainTxnMaxDuration") Duration domainTxnMaxDuration,
|
||||
BsaLock bsaLock,
|
||||
Clock clock,
|
||||
Response response) {
|
||||
this.scheduler = scheduler;
|
||||
this.gcsClient = gcsClient;
|
||||
this.bsaReportSender = bsaReportSender;
|
||||
this.transactionBatchSize = transactionBatchSize;
|
||||
this.domainTxnMaxDuration = domainTxnMaxDuration;
|
||||
this.bsaLock = bsaLock;
|
||||
this.clock = clock;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
if (!bsaLock.executeWithLock(this::runWithinLock)) {
|
||||
logger.atInfo().log("Job is being executed by another worker.");
|
||||
}
|
||||
} catch (Throwable throwable) {
|
||||
// TODO(12/31/2023): consider sending an alert email.
|
||||
logger.atWarning().withCause(throwable).log("Failed to update block lists.");
|
||||
}
|
||||
// Always return OK. No need to use a retrier on `runWithinLock`. Its individual steps are
|
||||
// implicitly retried. If action fails, the next cron will continue at checkpoint.
|
||||
response.setStatus(SC_OK);
|
||||
}
|
||||
|
||||
/** Executes the refresh action while holding the BSA lock. */
|
||||
Void runWithinLock() {
|
||||
// Cannot enroll new TLDs after download starts. This may change if b/309175410 is fixed.
|
||||
if (!Tlds.hasActiveBsaEnrollment(clock.nowUtc())) {
|
||||
logger.atInfo().log("No TLDs enrolled with BSA. Quitting.");
|
||||
return null;
|
||||
}
|
||||
Optional<RefreshSchedule> maybeSchedule = scheduler.schedule();
|
||||
if (!maybeSchedule.isPresent()) {
|
||||
logger.atInfo().log("No completed downloads yet. Exiting.");
|
||||
return null;
|
||||
}
|
||||
RefreshSchedule schedule = maybeSchedule.get();
|
||||
DomainsRefresher refresher =
|
||||
new DomainsRefresher(
|
||||
schedule.prevRefreshTime(), clock.nowUtc(), domainTxnMaxDuration, transactionBatchSize);
|
||||
switch (schedule.stage()) {
|
||||
case CHECK_FOR_CHANGES:
|
||||
ImmutableList<UnblockableDomainChange> blockabilityChanges =
|
||||
refresher.checkForBlockabilityChanges();
|
||||
if (blockabilityChanges.isEmpty()) {
|
||||
logger.atInfo().log("No change to Unblockable domains found.");
|
||||
schedule.updateJobStage(RefreshStage.DONE);
|
||||
return null;
|
||||
}
|
||||
gcsClient.writeRefreshChanges(schedule.jobName(), blockabilityChanges.stream());
|
||||
schedule.updateJobStage(RefreshStage.APPLY_CHANGES);
|
||||
// Fall through
|
||||
case APPLY_CHANGES:
|
||||
try (Stream<UnblockableDomainChange> changes =
|
||||
gcsClient.readRefreshChanges(schedule.jobName())) {
|
||||
BatchedStreams.toBatches(changes, 500).forEach(refresher::applyUnblockableChanges);
|
||||
}
|
||||
schedule.updateJobStage(RefreshStage.UPLOAD_REMOVALS);
|
||||
// Fall through
|
||||
case UPLOAD_REMOVALS:
|
||||
try (Stream<UnblockableDomainChange> changes =
|
||||
gcsClient.readRefreshChanges(schedule.jobName())) {
|
||||
Optional<String> report =
|
||||
JsonSerializations.toUnblockableDomainsRemovalReport(
|
||||
changes
|
||||
.filter(UnblockableDomainChange::isDelete)
|
||||
.map(UnblockableDomainChange::domainName));
|
||||
if (report.isPresent()) {
|
||||
gcsClient.logRemovedUnblockableDomainsReport(
|
||||
schedule.jobName(), LINE_SPLITTER.splitToStream(report.get()));
|
||||
bsaReportSender.removeUnblockableDomainsUpdates(report.get());
|
||||
} else {
|
||||
logger.atInfo().log("No Unblockable domains to remove.");
|
||||
}
|
||||
}
|
||||
schedule.updateJobStage(RefreshStage.UPLOAD_ADDITIONS);
|
||||
// Fall through
|
||||
case UPLOAD_ADDITIONS:
|
||||
try (Stream<UnblockableDomainChange> changes =
|
||||
gcsClient.readRefreshChanges(schedule.jobName())) {
|
||||
Optional<String> report =
|
||||
JsonSerializations.toUnblockableDomainsReport(
|
||||
changes
|
||||
.filter(UnblockableDomainChange::AddOrChange)
|
||||
.map(UnblockableDomainChange::newValue));
|
||||
if (report.isPresent()) {
|
||||
gcsClient.logRemovedUnblockableDomainsReport(
|
||||
schedule.jobName(), LINE_SPLITTER.splitToStream(report.get()));
|
||||
bsaReportSender.removeUnblockableDomainsUpdates(report.get());
|
||||
} else {
|
||||
logger.atInfo().log("No new Unblockable domains to add.");
|
||||
}
|
||||
}
|
||||
schedule.updateJobStage(RefreshStage.DONE);
|
||||
break;
|
||||
case DONE:
|
||||
logger.atInfo().log("Unexpectedly reaching the `DONE` stage.");
|
||||
break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
45
core/src/main/java/google/registry/bsa/BsaStringUtils.java
Normal file
45
core/src/main/java/google/registry/bsa/BsaStringUtils.java
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2023 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.bsa;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import java.util.List;
|
||||
|
||||
/** Helpers for domain name manipulation and string serialization of Java objects. */
|
||||
public class BsaStringUtils {
|
||||
|
||||
public static final Joiner DOMAIN_JOINER = Joiner.on('.');
|
||||
public static final Joiner PROPERTY_JOINER = Joiner.on(',');
|
||||
public static final Splitter DOMAIN_SPLITTER = Splitter.on('.');
|
||||
public static final Splitter PROPERTY_SPLITTER = Splitter.on(',');
|
||||
public static final Splitter LINE_SPLITTER = Splitter.on('\n');
|
||||
|
||||
public static String getLabelInDomain(String domainName) {
|
||||
List<String> parts = DOMAIN_SPLITTER.limit(1).splitToList(domainName);
|
||||
checkArgument(!parts.isEmpty(), "Not a valid domain: [%s]", domainName);
|
||||
return parts.get(0);
|
||||
}
|
||||
|
||||
public static String getTldInDomain(String domainName) {
|
||||
List<String> parts = DOMAIN_SPLITTER.splitToList(domainName);
|
||||
checkArgument(parts.size() == 2, "Not a valid domain: [%s]", domainName);
|
||||
return parts.get(1);
|
||||
}
|
||||
|
||||
private BsaStringUtils() {}
|
||||
}
|
||||
42
core/src/main/java/google/registry/bsa/BsaTransactions.java
Normal file
42
core/src/main/java/google/registry/bsa/BsaTransactions.java
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2023 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.bsa;
|
||||
|
||||
import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
/**
|
||||
* Helpers for executing JPA transactions for BSA processing.
|
||||
*
|
||||
* <p>All mutating transactions for BSA may be executed at the {@code TRANSACTION_REPEATABLE_READ}
|
||||
* level.
|
||||
*/
|
||||
public final class BsaTransactions {
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
public static <T> T bsaTransact(Callable<T> work) {
|
||||
return tm().transact(work, TRANSACTION_REPEATABLE_READ);
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
public static <T> T bsaQuery(Callable<T> work) {
|
||||
return tm().transact(work, TRANSACTION_REPEATABLE_READ);
|
||||
}
|
||||
|
||||
private BsaTransactions() {}
|
||||
}
|
||||
@@ -14,23 +14,32 @@
|
||||
|
||||
package google.registry.bsa;
|
||||
|
||||
import google.registry.bsa.api.BlockLabel;
|
||||
import google.registry.bsa.api.BlockOrder;
|
||||
|
||||
/** The processing stages of a download. */
|
||||
public enum DownloadStage {
|
||||
/** Downloads BSA block list files. */
|
||||
DOWNLOAD,
|
||||
/** Generates block list diffs with the previous download. */
|
||||
MAKE_DIFF,
|
||||
/** Applies the label diffs to the database tables. */
|
||||
APPLY_DIFF,
|
||||
DOWNLOAD_BLOCK_LISTS,
|
||||
/**
|
||||
* Generates block list diffs against the previous download. The diffs consist of a stream of
|
||||
* {@link BlockOrder orders} and a stream of {@link BlockLabel labels}.
|
||||
*/
|
||||
MAKE_ORDER_AND_LABEL_DIFF,
|
||||
/** Applies the diffs to the database. */
|
||||
APPLY_ORDER_AND_LABEL_DIFF,
|
||||
/**
|
||||
* Makes a REST API call to BSA endpoint, declaring that processing starts for new orders in the
|
||||
* diffs.
|
||||
*/
|
||||
START_UPLOADING,
|
||||
/** Makes a REST API call to BSA endpoint, sending the domains that cannot be blocked. */
|
||||
UPLOAD_DOMAINS_IN_USE,
|
||||
REPORT_START_OF_ORDER_PROCESSING,
|
||||
/**
|
||||
* Makes a REST API call to BSA endpoint, uploading unblockable domains that match labels in the
|
||||
* diff.
|
||||
*/
|
||||
UPLOAD_UNBLOCKABLE_DOMAINS_FOR_NEW_ORDERS,
|
||||
/** Makes a REST API call to BSA endpoint, declaring the completion of order processing. */
|
||||
FINISH_UPLOADING,
|
||||
REPORT_END_OF_ORDER_PROCESSING,
|
||||
/** The terminal stage after processing succeeds. */
|
||||
DONE,
|
||||
/**
|
||||
@@ -42,5 +51,5 @@ public enum DownloadStage {
|
||||
* The terminal stage indicating that the downloads are not processed because their BSA-generated
|
||||
* checksums do not match those calculated by us.
|
||||
*/
|
||||
CHECKSUMS_NOT_MATCH;
|
||||
CHECKSUMS_DO_NOT_MATCH;
|
||||
}
|
||||
|
||||
229
core/src/main/java/google/registry/bsa/GcsClient.java
Normal file
229
core/src/main/java/google/registry/bsa/GcsClient.java
Normal file
@@ -0,0 +1,229 @@
|
||||
// Copyright 2023 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.bsa;
|
||||
|
||||
import static com.google.common.io.BaseEncoding.base16;
|
||||
|
||||
import com.google.cloud.storage.BlobId;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.bsa.BlockListFetcher.LazyBlockList;
|
||||
import google.registry.bsa.api.BlockLabel;
|
||||
import google.registry.bsa.api.BlockOrder;
|
||||
import google.registry.bsa.api.UnblockableDomain;
|
||||
import google.registry.bsa.api.UnblockableDomainChange;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.gcs.GcsUtils;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.stream.Stream;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/** Stores and accesses BSA-related data, including original downloads and processed data. */
|
||||
public class GcsClient {
|
||||
|
||||
// Intermediate data files:
|
||||
static final String LABELS_DIFF_FILE = "labels_diff.csv";
|
||||
static final String ORDERS_DIFF_FILE = "orders_diff.csv";
|
||||
static final String UNBLOCKABLE_DOMAINS_FILE = "unblockable_domains.csv";
|
||||
static final String REFRESHED_UNBLOCKABLE_DOMAINS_FILE = "refreshed_unblockable_domains.csv";
|
||||
|
||||
// Logged report data sent to BSA.
|
||||
static final String IN_PROGRESS_ORDERS_REPORT = "in_progress_orders.json";
|
||||
static final String COMPLETED_ORDERS_REPORT = "completed_orders.json";
|
||||
static final String ADDED_UNBLOCKABLE_DOMAINS_REPORT = "added_unblockable_domains.json";
|
||||
static final String REMOVED_UNBLOCKABLE_DOMAINS_REPORT = "removed_unblockable_domains.json";
|
||||
|
||||
private final GcsUtils gcsUtils;
|
||||
private final String bucketName;
|
||||
|
||||
private final String checksumAlgorithm;
|
||||
|
||||
@Inject
|
||||
GcsClient(
|
||||
GcsUtils gcsUtils,
|
||||
@Config("bsaGcsBucket") String bucketName,
|
||||
@Config("bsaChecksumAlgorithm") String checksumAlgorithm) {
|
||||
this.gcsUtils = gcsUtils;
|
||||
this.bucketName = bucketName;
|
||||
this.checksumAlgorithm = checksumAlgorithm;
|
||||
}
|
||||
|
||||
static String getBlockListFileName(BlockListType blockListType) {
|
||||
return blockListType.name() + ".csv";
|
||||
}
|
||||
|
||||
ImmutableMap<BlockListType, String> saveAndChecksumBlockList(
|
||||
String jobName, ImmutableList<LazyBlockList> blockLists) {
|
||||
// Downloading sequentially, since one is expected to be much smaller than the other.
|
||||
return blockLists.stream()
|
||||
.collect(
|
||||
ImmutableMap.toImmutableMap(
|
||||
LazyBlockList::getName, blockList -> saveAndChecksumBlockList(jobName, blockList)));
|
||||
}
|
||||
|
||||
private String saveAndChecksumBlockList(String jobName, LazyBlockList blockList) {
|
||||
BlobId blobId = getBlobId(jobName, getBlockListFileName(blockList.getName()));
|
||||
try (BufferedOutputStream gcsWriter =
|
||||
new BufferedOutputStream(gcsUtils.openOutputStream(blobId))) {
|
||||
MessageDigest messageDigest = MessageDigest.getInstance(checksumAlgorithm);
|
||||
blockList.consumeAll(
|
||||
(byteArray, length) -> {
|
||||
try {
|
||||
gcsWriter.write(byteArray, 0, length);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
messageDigest.update(byteArray, 0, length);
|
||||
});
|
||||
return base16().lowerCase().encode(messageDigest.digest());
|
||||
} catch (IOException | NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeWithNewline(BufferedWriter writer, String line) {
|
||||
try {
|
||||
writer.write(line);
|
||||
if (!line.endsWith("\n")) {
|
||||
writer.write('\n');
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
Stream<String> readBlockList(String jobName, BlockListType blockListType) {
|
||||
return readStream(getBlobId(jobName, getBlockListFileName(blockListType)));
|
||||
}
|
||||
|
||||
Stream<BlockOrder> readOrderDiffs(String jobName) {
|
||||
BlobId blobId = getBlobId(jobName, ORDERS_DIFF_FILE);
|
||||
return readStream(blobId).map(BlockOrder::deserialize);
|
||||
}
|
||||
|
||||
void writeOrderDiffs(String jobName, Stream<BlockOrder> orders) {
|
||||
BlobId blobId = getBlobId(jobName, ORDERS_DIFF_FILE);
|
||||
try (BufferedWriter gcsWriter = getWriter(blobId)) {
|
||||
orders.map(BlockOrder::serialize).forEach(line -> writeWithNewline(gcsWriter, line));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
Stream<BlockLabel> readLabelDiffs(String jobName) {
|
||||
BlobId blobId = getBlobId(jobName, LABELS_DIFF_FILE);
|
||||
return readStream(blobId).map(BlockLabel::deserialize);
|
||||
}
|
||||
|
||||
void writeLabelDiffs(String jobName, Stream<BlockLabel> labels) {
|
||||
BlobId blobId = getBlobId(jobName, LABELS_DIFF_FILE);
|
||||
try (BufferedWriter gcsWriter = getWriter(blobId)) {
|
||||
labels.map(BlockLabel::serialize).forEach(line -> writeWithNewline(gcsWriter, line));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
Stream<UnblockableDomain> readUnblockableDomains(String jobName) {
|
||||
BlobId blobId = getBlobId(jobName, UNBLOCKABLE_DOMAINS_FILE);
|
||||
return readStream(blobId).map(UnblockableDomain::deserialize);
|
||||
}
|
||||
|
||||
void writeUnblockableDomains(String jobName, Stream<UnblockableDomain> unblockables) {
|
||||
BlobId blobId = getBlobId(jobName, UNBLOCKABLE_DOMAINS_FILE);
|
||||
try (BufferedWriter gcsWriter = getWriter(blobId)) {
|
||||
unblockables
|
||||
.map(UnblockableDomain::serialize)
|
||||
.forEach(line -> writeWithNewline(gcsWriter, line));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
Stream<UnblockableDomainChange> readRefreshChanges(String jobName) {
|
||||
BlobId blobId = getBlobId(jobName, UNBLOCKABLE_DOMAINS_FILE);
|
||||
return readStream(blobId).map(UnblockableDomainChange::deserialize);
|
||||
}
|
||||
|
||||
void writeRefreshChanges(String jobName, Stream<UnblockableDomainChange> changes) {
|
||||
BlobId blobId = getBlobId(jobName, REFRESHED_UNBLOCKABLE_DOMAINS_FILE);
|
||||
try (BufferedWriter gcsWriter = getWriter(blobId)) {
|
||||
changes
|
||||
.map(UnblockableDomainChange::serialize)
|
||||
.forEach(line -> writeWithNewline(gcsWriter, line));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
void logInProgressOrderReport(String jobName, Stream<String> lines) {
|
||||
BlobId blobId = getBlobId(jobName, IN_PROGRESS_ORDERS_REPORT);
|
||||
try (BufferedWriter gcsWriter = getWriter(blobId)) {
|
||||
lines.forEach(line -> writeWithNewline(gcsWriter, line));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
void logCompletedOrderReport(String jobName, Stream<String> lines) {
|
||||
BlobId blobId = getBlobId(jobName, COMPLETED_ORDERS_REPORT);
|
||||
try (BufferedWriter gcsWriter = getWriter(blobId)) {
|
||||
lines.forEach(line -> writeWithNewline(gcsWriter, line));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
void logAddedUnblockableDomainsReport(String jobName, Stream<String> lines) {
|
||||
BlobId blobId = getBlobId(jobName, ADDED_UNBLOCKABLE_DOMAINS_REPORT);
|
||||
try (BufferedWriter gcsWriter = getWriter(blobId)) {
|
||||
lines.forEach(line -> writeWithNewline(gcsWriter, line));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
void logRemovedUnblockableDomainsReport(String jobName, Stream<String> lines) {
|
||||
BlobId blobId = getBlobId(jobName, REMOVED_UNBLOCKABLE_DOMAINS_REPORT);
|
||||
try (BufferedWriter gcsWriter = getWriter(blobId)) {
|
||||
lines.forEach(line -> writeWithNewline(gcsWriter, line));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
BlobId getBlobId(String folder, String name) {
|
||||
return BlobId.of(bucketName, String.format("%s/%s", folder, name));
|
||||
}
|
||||
|
||||
Stream<String> readStream(BlobId blobId) {
|
||||
return new BufferedReader(
|
||||
new InputStreamReader(gcsUtils.openInputStream(blobId), StandardCharsets.UTF_8))
|
||||
.lines();
|
||||
}
|
||||
|
||||
BufferedWriter getWriter(BlobId blobId) {
|
||||
return new BufferedWriter(
|
||||
new OutputStreamWriter(gcsUtils.openOutputStream(blobId), StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
@@ -16,13 +16,12 @@ package google.registry.bsa;
|
||||
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.Maps.transformValues;
|
||||
import static google.registry.model.tld.Tld.isEnrolledWithBsa;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Sets.SetView;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.model.tld.Tld.TldType;
|
||||
import google.registry.model.tld.Tlds;
|
||||
@@ -50,14 +49,6 @@ public class IdnChecker {
|
||||
allTlds = idnToTlds.values().stream().flatMap(ImmutableSet::stream).collect(toImmutableSet());
|
||||
}
|
||||
|
||||
// TODO(11/30/2023): Remove below when new Tld schema is deployed and the `getBsaEnrollStartTime`
|
||||
// method is no longer hardcoded.
|
||||
@VisibleForTesting
|
||||
IdnChecker(ImmutableMap<IdnTableEnum, ImmutableSet<Tld>> idnToTlds) {
|
||||
this.idnToTlds = idnToTlds;
|
||||
allTlds = idnToTlds.values().stream().flatMap(ImmutableSet::stream).collect(toImmutableSet());
|
||||
}
|
||||
|
||||
/** Returns all IDNs in which the {@code label} is valid. */
|
||||
ImmutableSet<IdnTableEnum> getAllValidIdns(String label) {
|
||||
return idnToTlds.keySet().stream()
|
||||
@@ -84,13 +75,8 @@ public class IdnChecker {
|
||||
*
|
||||
* @param idnTables String names of {@link IdnTableEnum} values
|
||||
*/
|
||||
public SetView<Tld> getForbiddingTlds(ImmutableSet<String> idnTables) {
|
||||
return Sets.difference(allTlds, getSupportingTlds(idnTables));
|
||||
}
|
||||
|
||||
private static boolean isEnrolledWithBsa(Tld tld, DateTime now) {
|
||||
DateTime enrollTime = tld.getBsaEnrollStartTime();
|
||||
return enrollTime != null && enrollTime.isBefore(now);
|
||||
public ImmutableSet<Tld> getForbiddingTlds(ImmutableSet<String> idnTables) {
|
||||
return Sets.difference(allTlds, getSupportingTlds(idnTables)).immutableCopy();
|
||||
}
|
||||
|
||||
private static ImmutableMap<IdnTableEnum, ImmutableSet<Tld>> getIdnToTldMap(DateTime now) {
|
||||
|
||||
30
core/src/main/java/google/registry/bsa/RefreshStage.java
Normal file
30
core/src/main/java/google/registry/bsa/RefreshStage.java
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2023 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.bsa;
|
||||
|
||||
public enum RefreshStage {
|
||||
/**
|
||||
* Checks for stale unblockable domains. The output is a stream of {@link
|
||||
* google.registry.bsa.api.UnblockableDomainChange} objects that describe the stale domains.
|
||||
*/
|
||||
CHECK_FOR_CHANGES,
|
||||
/** Fixes the stale domains in the database. */
|
||||
APPLY_CHANGES,
|
||||
/** Reports the unblockable domains to be removed to BSA. */
|
||||
UPLOAD_REMOVALS,
|
||||
/** Reports the newly found unblockable domains to BSA. */
|
||||
UPLOAD_ADDITIONS,
|
||||
DONE;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2023 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.bsa;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.bsa.BsaStringUtils.DOMAIN_JOINER;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.isReserved;
|
||||
import static google.registry.model.tld.Tlds.findTldForName;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.net.InternetDomainName;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.model.tld.Tld.TldState;
|
||||
import google.registry.model.tld.Tld.TldType;
|
||||
import google.registry.model.tld.Tlds;
|
||||
import google.registry.model.tld.label.ReservedList;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* Utility for looking up reserved domain names.
|
||||
*
|
||||
* <p>This utility is only concerned with reserved domains that can be created (with appropriate
|
||||
* tokens).
|
||||
*/
|
||||
public final class ReservedDomainsUtils {
|
||||
|
||||
private ReservedDomainsUtils() {}
|
||||
|
||||
public static Stream<String> getAllReservedNames(DateTime now) {
|
||||
return Tlds.getTldEntitiesOfType(TldType.REAL).stream()
|
||||
.filter(tld -> Tld.isEnrolledWithBsa(tld, now))
|
||||
.map(tld -> getAllReservedDomainsInTld(tld, now))
|
||||
.flatMap(ImmutableSet::stream);
|
||||
}
|
||||
|
||||
/** Returns */
|
||||
static ImmutableSet<String> getAllReservedDomainsInTld(Tld tld, DateTime now) {
|
||||
return tld.getReservedListNames().stream()
|
||||
.map(ReservedList::get)
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.map(ReservedList::getReservedListEntries)
|
||||
.map(Map::keySet)
|
||||
.flatMap(Set::stream)
|
||||
.map(label -> DOMAIN_JOINER.join(label, tld.getTldStr()))
|
||||
.filter(domain -> isReservedDomain(domain, now))
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if {@code domain} is a reserved name that can be registered right now (e.g.,
|
||||
* during sunrise or with allocation token), therefore unblockable.
|
||||
*/
|
||||
public static boolean isReservedDomain(String domain, DateTime now) {
|
||||
Optional<InternetDomainName> tldStr = findTldForName(InternetDomainName.from(domain));
|
||||
verify(tldStr.isPresent(), "Tld for domain [%s] unexpectedly missing.", domain);
|
||||
Tld tld = Tld.get(tldStr.get().toString());
|
||||
return isReserved(
|
||||
InternetDomainName.from(domain),
|
||||
Objects.equals(tld.getTldState(now), TldState.START_DATE_SUNRISE));
|
||||
}
|
||||
}
|
||||
64
core/src/main/java/google/registry/bsa/api/BlockLabel.java
Normal file
64
core/src/main/java/google/registry/bsa/api/BlockLabel.java
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright 2023 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.bsa.api;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A BSA label to block. New domains with matching second-level domain (SLD) will be denied
|
||||
* registration in TLDs enrolled with BSA.
|
||||
*/
|
||||
@AutoValue
|
||||
public abstract class BlockLabel {
|
||||
|
||||
static final Joiner JOINER = Joiner.on(',');
|
||||
static final Splitter SPLITTER = Splitter.on(',').trimResults();
|
||||
|
||||
public abstract String label();
|
||||
|
||||
public abstract LabelType labelType();
|
||||
|
||||
public abstract ImmutableSet<String> idnTables();
|
||||
|
||||
public String serialize() {
|
||||
return JOINER.join(label(), labelType().name(), idnTables().stream().sorted().toArray());
|
||||
}
|
||||
|
||||
public static BlockLabel deserialize(String text) {
|
||||
List<String> items = SPLITTER.splitToList(text);
|
||||
try {
|
||||
return of(
|
||||
items.get(0),
|
||||
LabelType.valueOf(items.get(1)),
|
||||
ImmutableSet.copyOf(items.subList(2, items.size())));
|
||||
} catch (NumberFormatException ne) {
|
||||
throw new IllegalArgumentException(text);
|
||||
}
|
||||
}
|
||||
|
||||
public static BlockLabel of(String label, LabelType type, ImmutableSet<String> idnTables) {
|
||||
return new AutoValue_BlockLabel(label, type, idnTables);
|
||||
}
|
||||
|
||||
public enum LabelType {
|
||||
CREATE,
|
||||
NEW_ORDER_ASSOCIATION,
|
||||
DELETE;
|
||||
}
|
||||
}
|
||||
57
core/src/main/java/google/registry/bsa/api/BlockOrder.java
Normal file
57
core/src/main/java/google/registry/bsa/api/BlockOrder.java
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright 2023 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.bsa.api;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A BSA order, which are needed when communicating with the BSA API while processing downloaded
|
||||
* block lists.
|
||||
*/
|
||||
@AutoValue
|
||||
public abstract class BlockOrder {
|
||||
|
||||
public abstract long orderId();
|
||||
|
||||
public abstract OrderType orderType();
|
||||
|
||||
static final Joiner JOINER = Joiner.on(',');
|
||||
static final Splitter SPLITTER = Splitter.on(',');
|
||||
|
||||
public String serialize() {
|
||||
return JOINER.join(orderId(), orderType().name());
|
||||
}
|
||||
|
||||
public static BlockOrder deserialize(String text) {
|
||||
List<String> items = SPLITTER.splitToList(text);
|
||||
try {
|
||||
return of(Long.valueOf(items.get(0)), OrderType.valueOf(items.get(1)));
|
||||
} catch (NumberFormatException ne) {
|
||||
throw new IllegalArgumentException(text);
|
||||
}
|
||||
}
|
||||
|
||||
public static BlockOrder of(long orderId, OrderType orderType) {
|
||||
return new AutoValue_BlockOrder(orderId, orderType);
|
||||
}
|
||||
|
||||
public enum OrderType {
|
||||
CREATE,
|
||||
DELETE;
|
||||
}
|
||||
}
|
||||
160
core/src/main/java/google/registry/bsa/api/BsaCredential.java
Normal file
160
core/src/main/java/google/registry/bsa/api/BsaCredential.java
Normal file
@@ -0,0 +1,160 @@
|
||||
// Copyright 2023 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.bsa.api;
|
||||
|
||||
import static google.registry.request.UrlConnectionUtils.getResponseBytes;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import com.google.api.client.http.HttpMethods;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.gson.Gson;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.keyring.api.Keyring;
|
||||
import google.registry.request.UrlConnectionService;
|
||||
import google.registry.request.UrlConnectionUtils;
|
||||
import google.registry.util.Clock;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
import javax.inject.Inject;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import org.joda.time.Duration;
|
||||
import org.joda.time.Instant;
|
||||
|
||||
/**
|
||||
* A credential for accessing the BSA API.
|
||||
*
|
||||
* <p>Fetches on-demand an auth token from BSA's auth http endpoint and caches it for repeated use
|
||||
* until the token expires (expiry set by BSA and recorded in the configuration file). An expired
|
||||
* token is refreshed only when requested. Token refreshing is blocking but thread-safe.
|
||||
*
|
||||
* <p>The token-fetching request authenticates itself with an API key, which is stored in the Secret
|
||||
* Manager.
|
||||
*/
|
||||
@ThreadSafe
|
||||
public class BsaCredential {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
/** Content type of the auth http request. */
|
||||
private static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
|
||||
/** Template of the auth http request's payload. User must provide an API key. */
|
||||
private static final String AUTH_REQ_BODY_TEMPLATE = "apiKey=%s&space=BSA";
|
||||
/** The variable name for the auth token in the returned json response. */
|
||||
public static final String ID_TOKEN = "id_token";
|
||||
|
||||
private final UrlConnectionService urlConnectionService;
|
||||
|
||||
private final String authUrl;
|
||||
|
||||
private final Duration authTokenExpiry;
|
||||
|
||||
private final Keyring keyring;
|
||||
|
||||
private final Clock clock;
|
||||
|
||||
@Nullable private String authToken;
|
||||
private Instant lastRefreshTime;
|
||||
|
||||
@Inject
|
||||
BsaCredential(
|
||||
UrlConnectionService urlConnectionService,
|
||||
@Config("bsaAuthUrl") String authUrl,
|
||||
@Config("bsaAuthTokenExpiry") Duration authTokenExpiry,
|
||||
Keyring keyring,
|
||||
Clock clock) {
|
||||
this.urlConnectionService = urlConnectionService;
|
||||
this.authUrl = authUrl;
|
||||
this.authTokenExpiry = authTokenExpiry;
|
||||
this.keyring = keyring;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the auth token for accessing the BSA API.
|
||||
*
|
||||
* <p>This method refreshes the token if it is expired, and is thread-safe..
|
||||
*/
|
||||
public String getAuthToken() {
|
||||
try {
|
||||
ensureAuthTokenValid();
|
||||
} catch (IOException e) {
|
||||
throw new BsaException(e, /* retriable= */ true);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new BsaException(e, /* retriable= */ false);
|
||||
}
|
||||
return this.authToken;
|
||||
}
|
||||
|
||||
private void ensureAuthTokenValid() throws IOException, GeneralSecurityException {
|
||||
Instant now = Instant.ofEpochMilli(clock.nowUtc().getMillis());
|
||||
if (authToken != null && lastRefreshTime.plus(authTokenExpiry).isAfter(now)) {
|
||||
logger.atInfo().log("AuthToken still valid, reusing.");
|
||||
return;
|
||||
}
|
||||
synchronized (this) {
|
||||
authToken = fetchNewAuthToken();
|
||||
lastRefreshTime = now;
|
||||
logger.atInfo().log("AuthToken refreshed at %s.", now);
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
String fetchNewAuthToken() throws IOException, GeneralSecurityException {
|
||||
String payload = String.format(AUTH_REQ_BODY_TEMPLATE, keyring.getBsaApiKey());
|
||||
URL url = new URL(authUrl);
|
||||
logger.atInfo().log("Fetching auth token from %s", url);
|
||||
HttpsURLConnection connection = null;
|
||||
try {
|
||||
connection = (HttpsURLConnection) urlConnectionService.createConnection(url);
|
||||
connection.setRequestMethod(HttpMethods.POST);
|
||||
UrlConnectionUtils.setPayload(connection, payload.getBytes(UTF_8), CONTENT_TYPE);
|
||||
int code = connection.getResponseCode();
|
||||
if (code != SC_OK) {
|
||||
String errorDetails;
|
||||
try {
|
||||
errorDetails = new String(getResponseBytes(connection), UTF_8);
|
||||
} catch (Exception e) {
|
||||
errorDetails = "Failed to retrieve error message: " + e.getMessage();
|
||||
}
|
||||
throw new BsaException(
|
||||
String.format(
|
||||
"Status code: [%s], error: [%s], details: [%s]",
|
||||
code, connection.getResponseMessage(), errorDetails),
|
||||
/* retriable= */ true);
|
||||
}
|
||||
// TODO: catch json syntax exception
|
||||
@SuppressWarnings("unchecked")
|
||||
String idToken =
|
||||
new Gson()
|
||||
.fromJson(new String(getResponseBytes(connection), UTF_8), Map.class)
|
||||
.getOrDefault(ID_TOKEN, "")
|
||||
.toString();
|
||||
if (idToken.isEmpty()) {
|
||||
throw new BsaException("Response missing ID token", /* retriable= */ false);
|
||||
}
|
||||
return idToken;
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
core/src/main/java/google/registry/bsa/api/BsaException.java
Normal file
34
core/src/main/java/google/registry/bsa/api/BsaException.java
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright 2023 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.bsa.api;
|
||||
|
||||
public class BsaException extends RuntimeException {
|
||||
|
||||
private final boolean retriable;
|
||||
|
||||
public BsaException(Throwable cause, boolean retriable) {
|
||||
super(cause);
|
||||
this.retriable = retriable;
|
||||
}
|
||||
|
||||
public BsaException(String message, boolean retriable) {
|
||||
super(message);
|
||||
this.retriable = retriable;
|
||||
}
|
||||
|
||||
public boolean isRetriable() {
|
||||
return this.retriable;
|
||||
}
|
||||
}
|
||||
127
core/src/main/java/google/registry/bsa/api/BsaReportSender.java
Normal file
127
core/src/main/java/google/registry/bsa/api/BsaReportSender.java
Normal file
@@ -0,0 +1,127 @@
|
||||
// Copyright 2023 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.bsa.api;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_ACCEPTED;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_OK;
|
||||
|
||||
import com.google.api.client.http.HttpMethods;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.request.UrlConnectionService;
|
||||
import google.registry.request.UrlConnectionUtils;
|
||||
import google.registry.util.Retrier;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.security.GeneralSecurityException;
|
||||
import javax.inject.Inject;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
||||
/**
|
||||
* Sends order processing reports to BSA.
|
||||
*
|
||||
* <p>Senders are responsible for keeping payloads at reasonable sizes.
|
||||
*/
|
||||
public class BsaReportSender {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private static final MediaType CONTENT_TYPE = MediaType.JSON_UTF_8;
|
||||
|
||||
private final UrlConnectionService urlConnectionService;
|
||||
private final BsaCredential credential;
|
||||
private final String orderStatusUrl;
|
||||
private final String addUnblockableDomainsUrl;
|
||||
private final String removeUnblockableDomainsUrl;
|
||||
|
||||
private final Retrier retrier;
|
||||
|
||||
@Inject
|
||||
BsaReportSender(
|
||||
UrlConnectionService urlConnectionService,
|
||||
BsaCredential credential,
|
||||
@Config("bsaOrderStatusUrl") String orderStatusUrl,
|
||||
@Config("bsaAddUnblockableDomainsUrl") String addUnblockableDomainsUrl,
|
||||
@Config("bsaRemoveUnblockableDomainsUrl") String removeUnblockableDomainsUrl,
|
||||
Retrier retrier) {
|
||||
this.urlConnectionService = urlConnectionService;
|
||||
this.credential = credential;
|
||||
this.orderStatusUrl = orderStatusUrl;
|
||||
this.addUnblockableDomainsUrl = addUnblockableDomainsUrl;
|
||||
this.removeUnblockableDomainsUrl = removeUnblockableDomainsUrl;
|
||||
this.retrier = retrier;
|
||||
}
|
||||
|
||||
public void sendOrderStatusReport(String payload) {
|
||||
retrier.callWithRetry(
|
||||
() -> trySendData(this.orderStatusUrl, payload),
|
||||
e -> e instanceof BsaException && ((BsaException) e).isRetriable());
|
||||
}
|
||||
|
||||
public void addUnblockableDomainsUpdates(String payload) {
|
||||
retrier.callWithRetry(
|
||||
() -> trySendData(this.addUnblockableDomainsUrl, payload),
|
||||
e -> e instanceof BsaException && ((BsaException) e).isRetriable());
|
||||
}
|
||||
|
||||
public void removeUnblockableDomainsUpdates(String payload) {
|
||||
retrier.callWithRetry(
|
||||
() -> trySendData(this.removeUnblockableDomainsUrl, payload),
|
||||
e -> e instanceof BsaException && ((BsaException) e).isRetriable());
|
||||
}
|
||||
|
||||
Void trySendData(String urlString, String payload) {
|
||||
try {
|
||||
URL url = new URL(urlString);
|
||||
HttpsURLConnection connection =
|
||||
(HttpsURLConnection) urlConnectionService.createConnection(url);
|
||||
connection.setRequestMethod(HttpMethods.POST);
|
||||
connection.setRequestProperty("Authorization", "Bearer " + credential.getAuthToken());
|
||||
UrlConnectionUtils.setPayload(connection, payload.getBytes(UTF_8), CONTENT_TYPE.toString());
|
||||
int code = connection.getResponseCode();
|
||||
if (code != SC_OK && code != SC_ACCEPTED) {
|
||||
String errorDetails = "";
|
||||
try (InputStream errorStream = connection.getErrorStream()) {
|
||||
errorDetails = new String(ByteStreams.toByteArray(errorStream), UTF_8);
|
||||
} catch (NullPointerException e) {
|
||||
// No error message.
|
||||
} catch (Exception e) {
|
||||
errorDetails = "Failed to retrieve error message: " + e.getMessage();
|
||||
}
|
||||
// TODO(b/318404541): sanitize errorDetails to prevent log injection attack.
|
||||
throw new BsaException(
|
||||
String.format(
|
||||
"Status code: [%s], error: [%s], details: [%s]",
|
||||
code, connection.getResponseMessage(), errorDetails),
|
||||
/* retriable= */ true);
|
||||
}
|
||||
try (InputStream errorStream = connection.getInputStream()) {
|
||||
String responseMessage = new String(ByteStreams.toByteArray(errorStream), UTF_8);
|
||||
logger.atInfo().log("Received response: [%s]", responseMessage);
|
||||
} catch (Exception e) {
|
||||
logger.atInfo().withCause(e).log("Failed to retrieve response message.");
|
||||
}
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
throw new BsaException(e, /* retriable= */ true);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new BsaException(e, /* retriable= */ false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
// Copyright 2023 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.bsa.api;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.Maps.newTreeMap;
|
||||
import static com.google.common.collect.Multimaps.newListMultimap;
|
||||
import static com.google.common.collect.Multimaps.toMultimap;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import google.registry.bsa.api.BlockOrder.OrderType;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/** Helpers for generating {@link BlockOrder} and {@link UnblockableDomain} reports. */
|
||||
public final class JsonSerializations {
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
private JsonSerializations() {}
|
||||
|
||||
public static Optional<String> toInProgressOrdersReport(Stream<BlockOrder> orders) {
|
||||
ImmutableList<ImmutableMap<String, Object>> maps =
|
||||
orders.map(JsonSerializations::asInProgressOrder).collect(toImmutableList());
|
||||
if (maps.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(GSON.toJson(maps));
|
||||
}
|
||||
|
||||
public static Optional<String> toCompletedOrdersReport(Stream<BlockOrder> orders) {
|
||||
ImmutableList<ImmutableMap<String, Object>> maps =
|
||||
orders.map(JsonSerializations::asCompletedOrder).collect(toImmutableList());
|
||||
if (maps.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(GSON.toJson(maps));
|
||||
}
|
||||
|
||||
public static Optional<String> toUnblockableDomainsReport(Stream<UnblockableDomain> domains) {
|
||||
ImmutableMultimap<String, String> reasonToNames =
|
||||
ImmutableMultimap.copyOf(
|
||||
domains.collect(
|
||||
toMultimap(
|
||||
domain -> domain.reason().name().toLowerCase(Locale.ROOT),
|
||||
UnblockableDomain::domainName,
|
||||
() -> newListMultimap(newTreeMap(), Lists::newArrayList))));
|
||||
|
||||
if (reasonToNames.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(GSON.toJson(reasonToNames.asMap()));
|
||||
}
|
||||
|
||||
public static Optional<String> toUnblockableDomainsRemovalReport(Stream<String> domainNames) {
|
||||
ImmutableList<String> domainsList = domainNames.collect(toImmutableList());
|
||||
if (domainsList.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(GSON.toJson(domainsList));
|
||||
}
|
||||
|
||||
private static ImmutableMap<String, Object> asInProgressOrder(BlockOrder order) {
|
||||
String status =
|
||||
order.orderType().equals(OrderType.CREATE) ? "ActivationInProgress" : "ReleaseInProgress";
|
||||
return ImmutableMap.of("blockOrderId", order.orderId(), "status", status);
|
||||
}
|
||||
|
||||
private static ImmutableMap<String, Object> asCompletedOrder(BlockOrder order) {
|
||||
String status = order.orderType().equals(OrderType.CREATE) ? "Active" : "Closed";
|
||||
return ImmutableMap.of("blockOrderId", order.orderId(), "status", status);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2023 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.bsa.api;
|
||||
|
||||
import static google.registry.bsa.BsaStringUtils.DOMAIN_JOINER;
|
||||
import static google.registry.bsa.BsaStringUtils.PROPERTY_JOINER;
|
||||
import static google.registry.bsa.BsaStringUtils.PROPERTY_SPLITTER;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A domain name whose second-level domain (SLD) matches a BSA label but is not blocked. It may be
|
||||
* already registered, or on the TLD's reserve list.
|
||||
*/
|
||||
// TODO(1/15/2024): rename to UnblockableDomain.
|
||||
@AutoValue
|
||||
public abstract class UnblockableDomain {
|
||||
abstract String domainName();
|
||||
|
||||
abstract Reason reason();
|
||||
|
||||
/** Reasons why a valid domain name cannot be blocked. */
|
||||
public enum Reason {
|
||||
REGISTERED,
|
||||
RESERVED,
|
||||
INVALID;
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
return PROPERTY_JOINER.join(domainName(), reason().name());
|
||||
}
|
||||
|
||||
public static UnblockableDomain deserialize(String text) {
|
||||
List<String> items = PROPERTY_SPLITTER.splitToList(text);
|
||||
return of(items.get(0), Reason.valueOf(items.get(1)));
|
||||
}
|
||||
|
||||
public static UnblockableDomain of(String domainName, Reason reason) {
|
||||
return new AutoValue_UnblockableDomain(domainName, reason);
|
||||
}
|
||||
|
||||
public static UnblockableDomain of(String label, String tld, Reason reason) {
|
||||
return of(DOMAIN_JOINER.join(label, tld), reason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright 2023 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.bsa.api;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static google.registry.bsa.BsaStringUtils.PROPERTY_JOINER;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.auto.value.extension.memoized.Memoized;
|
||||
import google.registry.bsa.BsaStringUtils;
|
||||
import google.registry.bsa.api.UnblockableDomain.Reason;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Change record of an {@link UnblockableDomain}. */
|
||||
@AutoValue
|
||||
public abstract class UnblockableDomainChange {
|
||||
|
||||
/**
|
||||
* The text used in place of an empty {@link #newReason()} when an instance is serialized to
|
||||
* string.
|
||||
*
|
||||
* <p>This value helps manual inspection of the change files, making it easier to `grep` for
|
||||
* deletions in BSA reports.
|
||||
*/
|
||||
private static final String DELETE_REASON_PLACEHOLDER = "IS_DELETE";
|
||||
|
||||
abstract UnblockableDomain unblockable();
|
||||
|
||||
abstract Optional<Reason> newReason();
|
||||
|
||||
public String domainName() {
|
||||
return unblockable().domainName();
|
||||
}
|
||||
|
||||
@Memoized
|
||||
public UnblockableDomain newValue() {
|
||||
verify(newReason().isPresent(), "Removed unblockable does not have new value.");
|
||||
return UnblockableDomain.of(unblockable().domainName(), newReason().get());
|
||||
}
|
||||
|
||||
public boolean AddOrChange() {
|
||||
return newReason().isPresent();
|
||||
}
|
||||
|
||||
public boolean isDelete() {
|
||||
return !this.AddOrChange();
|
||||
}
|
||||
|
||||
public boolean isNew() {
|
||||
return newReason().filter(unblockable().reason()::equals).isPresent();
|
||||
}
|
||||
|
||||
public String serialize() {
|
||||
return PROPERTY_JOINER.join(
|
||||
unblockable().domainName(),
|
||||
unblockable().reason(),
|
||||
newReason().map(Reason::name).orElse(DELETE_REASON_PLACEHOLDER));
|
||||
}
|
||||
|
||||
public static UnblockableDomainChange deserialize(String text) {
|
||||
List<String> items = BsaStringUtils.PROPERTY_SPLITTER.splitToList(text);
|
||||
return of(
|
||||
UnblockableDomain.of(items.get(0), Reason.valueOf(items.get(1))),
|
||||
Objects.equals(items.get(2), DELETE_REASON_PLACEHOLDER)
|
||||
? Optional.empty()
|
||||
: Optional.of(Reason.valueOf(items.get(2))));
|
||||
}
|
||||
|
||||
public static UnblockableDomainChange ofNew(UnblockableDomain unblockable) {
|
||||
return of(unblockable, Optional.of(unblockable.reason()));
|
||||
}
|
||||
|
||||
public static UnblockableDomainChange ofDeleted(UnblockableDomain unblockable) {
|
||||
return of(unblockable, Optional.empty());
|
||||
}
|
||||
|
||||
public static UnblockableDomainChange ofChanged(UnblockableDomain unblockable, Reason newReason) {
|
||||
return of(unblockable, Optional.of(newReason));
|
||||
}
|
||||
|
||||
private static UnblockableDomainChange of(
|
||||
UnblockableDomain unblockable, Optional<Reason> newReason) {
|
||||
return new AutoValue_UnblockableDomainChange(unblockable, newReason);
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
// Copyright 2023 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.bsa.persistence;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import google.registry.bsa.persistence.BsaDomainInUse.BsaDomainInUseId;
|
||||
import google.registry.model.CreateAutoTimestamp;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.io.Serializable;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.IdClass;
|
||||
|
||||
/** A domain matching a BSA label but is in use (registered or reserved), so cannot be blocked. */
|
||||
@Entity
|
||||
@IdClass(BsaDomainInUseId.class)
|
||||
public class BsaDomainInUse {
|
||||
@Id String label;
|
||||
@Id String tld;
|
||||
|
||||
@Column(nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
Reason reason;
|
||||
|
||||
/**
|
||||
* Creation time of this record, which is the most recent time when the domain was detected to be
|
||||
* in use wrt BSA. It may be during the processing of a download, or during some other job that
|
||||
* refreshes the state.
|
||||
*
|
||||
* <p>This field is for information only.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Column(nullable = false)
|
||||
CreateAutoTimestamp createTime = CreateAutoTimestamp.create(null);
|
||||
|
||||
// For Hibernate
|
||||
BsaDomainInUse() {}
|
||||
|
||||
public BsaDomainInUse(String label, String tld, Reason reason) {
|
||||
this.label = label;
|
||||
this.tld = tld;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof BsaDomainInUse)) {
|
||||
return false;
|
||||
}
|
||||
BsaDomainInUse that = (BsaDomainInUse) o;
|
||||
return Objects.equal(label, that.label)
|
||||
&& Objects.equal(tld, that.tld)
|
||||
&& reason == that.reason
|
||||
&& Objects.equal(createTime, that.createTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(label, tld, reason, createTime);
|
||||
}
|
||||
|
||||
enum Reason {
|
||||
REGISTERED,
|
||||
RESERVED;
|
||||
}
|
||||
|
||||
static class BsaDomainInUseId implements Serializable {
|
||||
|
||||
private String label;
|
||||
private String tld;
|
||||
|
||||
// For Hibernate
|
||||
BsaDomainInUseId() {}
|
||||
|
||||
BsaDomainInUseId(String label, String tld) {
|
||||
this.label = label;
|
||||
this.tld = tld;
|
||||
}
|
||||
}
|
||||
|
||||
static VKey<BsaDomainInUse> vKey(String label, String tld) {
|
||||
return VKey.create(BsaDomainInUse.class, new BsaDomainInUseId(label, tld));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright 2023 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.bsa.persistence;
|
||||
|
||||
import static google.registry.bsa.RefreshStage.CHECK_FOR_CHANGES;
|
||||
import static google.registry.bsa.RefreshStage.DONE;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import google.registry.bsa.RefreshStage;
|
||||
import google.registry.model.CreateAutoTimestamp;
|
||||
import google.registry.model.UpdateAutoTimestamp;
|
||||
import google.registry.persistence.VKey;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/**
|
||||
* Records of completed and ongoing refresh actions, which recomputes the set of unblockable domains
|
||||
* and reports changes to BSA.
|
||||
*
|
||||
* <p>The refresh action only handles registered and reserved domain names. Invalid names only
|
||||
* change status when the IDN tables change, and will be handled by a separate tool when it happens.
|
||||
*/
|
||||
@Entity
|
||||
class BsaDomainRefresh {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
Long jobId;
|
||||
|
||||
@Column(nullable = false)
|
||||
CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null);
|
||||
|
||||
@Column(nullable = false)
|
||||
UpdateAutoTimestamp updateTime = UpdateAutoTimestamp.create(null);
|
||||
|
||||
@Column(nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
RefreshStage stage = CHECK_FOR_CHANGES;
|
||||
|
||||
BsaDomainRefresh() {}
|
||||
|
||||
long getJobId() {
|
||||
return jobId;
|
||||
}
|
||||
|
||||
DateTime getCreationTime() {
|
||||
return creationTime.getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the starting time of this job as a string, which can be used as folder name on GCS when
|
||||
* storing download data.
|
||||
*/
|
||||
String getJobName() {
|
||||
return getCreationTime().toString() + "-refresh";
|
||||
}
|
||||
|
||||
boolean isDone() {
|
||||
return java.util.Objects.equals(stage, DONE);
|
||||
}
|
||||
|
||||
RefreshStage getStage() {
|
||||
return this.stage;
|
||||
}
|
||||
|
||||
BsaDomainRefresh setStage(RefreshStage refreshStage) {
|
||||
this.stage = refreshStage;
|
||||
return this;
|
||||
}
|
||||
|
||||
VKey<BsaDomainRefresh> vKey() {
|
||||
return vKey(jobId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof BsaDomainRefresh)) {
|
||||
return false;
|
||||
}
|
||||
BsaDomainRefresh that = (BsaDomainRefresh) o;
|
||||
return Objects.equal(jobId, that.jobId)
|
||||
&& Objects.equal(creationTime, that.creationTime)
|
||||
&& Objects.equal(updateTime, that.updateTime)
|
||||
&& stage == that.stage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(jobId, creationTime, updateTime, stage);
|
||||
}
|
||||
|
||||
static VKey<BsaDomainRefresh> vKey(long jobId) {
|
||||
return VKey.create(BsaDomainRefresh.class, jobId);
|
||||
}
|
||||
}
|
||||
@@ -15,18 +15,20 @@
|
||||
package google.registry.bsa.persistence;
|
||||
|
||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||
import static google.registry.bsa.DownloadStage.DOWNLOAD;
|
||||
import static google.registry.bsa.DownloadStage.DONE;
|
||||
import static google.registry.bsa.DownloadStage.DOWNLOAD_BLOCK_LISTS;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import google.registry.bsa.BlockList;
|
||||
import google.registry.bsa.BlockListType;
|
||||
import google.registry.bsa.DownloadStage;
|
||||
import google.registry.model.CreateAutoTimestamp;
|
||||
import google.registry.model.UpdateAutoTimestamp;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.util.Locale;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EnumType;
|
||||
@@ -41,7 +43,7 @@ import org.joda.time.DateTime;
|
||||
/** Records of ongoing and completed download jobs. */
|
||||
@Entity
|
||||
@Table(indexes = {@Index(columnList = "creationTime")})
|
||||
public class BsaDownload {
|
||||
class BsaDownload {
|
||||
|
||||
private static final Joiner CSV_JOINER = Joiner.on(',');
|
||||
private static final Splitter CSV_SPLITTER = Splitter.on(',');
|
||||
@@ -61,7 +63,7 @@ public class BsaDownload {
|
||||
|
||||
@Column(nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
DownloadStage stage = DOWNLOAD;
|
||||
DownloadStage stage = DOWNLOAD_BLOCK_LISTS;
|
||||
|
||||
BsaDownload() {}
|
||||
|
||||
@@ -74,14 +76,21 @@ public class BsaDownload {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the starting time of this job as a string, which can be used as folder name on GCS when
|
||||
* storing download data.
|
||||
* Returns a unique name of the job.
|
||||
*
|
||||
* <p>The returned value should be a valid GCS folder name, consisting of only lower case
|
||||
* alphanumerics, underscore, hyphen and dot.
|
||||
*/
|
||||
public String getJobName() {
|
||||
return getCreationTime().toString();
|
||||
String getJobName() {
|
||||
// Return a value based on job start time, which is unique.
|
||||
return getCreationTime().toString().toLowerCase(Locale.ROOT).replace(":", "");
|
||||
}
|
||||
|
||||
public DownloadStage getStage() {
|
||||
boolean isDone() {
|
||||
return java.util.Objects.equals(stage, DONE);
|
||||
}
|
||||
|
||||
DownloadStage getStage() {
|
||||
return this.stage;
|
||||
}
|
||||
|
||||
@@ -90,19 +99,20 @@ public class BsaDownload {
|
||||
return this;
|
||||
}
|
||||
|
||||
BsaDownload setChecksums(ImmutableMap<BlockList, String> checksums) {
|
||||
BsaDownload setChecksums(ImmutableMap<BlockListType, String> checksums) {
|
||||
blockListChecksums =
|
||||
CSV_JOINER.withKeyValueSeparator("=").join(ImmutableSortedMap.copyOf(checksums));
|
||||
return this;
|
||||
}
|
||||
|
||||
ImmutableMap<BlockList, String> getChecksums() {
|
||||
ImmutableMap<BlockListType, String> getChecksums() {
|
||||
if (blockListChecksums.isEmpty()) {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
return CSV_SPLITTER.withKeyValueSeparator('=').split(blockListChecksums).entrySet().stream()
|
||||
.collect(
|
||||
toImmutableMap(entry -> BlockList.valueOf(entry.getKey()), entry -> entry.getValue()));
|
||||
toImmutableMap(
|
||||
entry -> BlockListType.valueOf(entry.getKey()), entry -> entry.getValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -28,7 +28,7 @@ import org.joda.time.DateTime;
|
||||
* <p>The label is valid (wrt IDN) in at least one TLD.
|
||||
*/
|
||||
@Entity
|
||||
public final class BsaLabel {
|
||||
final class BsaLabel {
|
||||
|
||||
@Id String label;
|
||||
|
||||
@@ -44,7 +44,7 @@ public final class BsaLabel {
|
||||
DateTime creationTime;
|
||||
|
||||
// For Hibernate.
|
||||
BsaLabel() {}
|
||||
private BsaLabel() {}
|
||||
|
||||
BsaLabel(String label, DateTime creationTime) {
|
||||
this.label = label;
|
||||
@@ -52,7 +52,7 @@ public final class BsaLabel {
|
||||
}
|
||||
|
||||
/** Returns the label to be blocked. */
|
||||
public String getLabel() {
|
||||
String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright 2023 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.bsa.persistence;
|
||||
|
||||
import static google.registry.config.RegistryConfig.getEppResourceCachingDuration;
|
||||
import static google.registry.config.RegistryConfig.getEppResourceMaxCachedEntries;
|
||||
import static google.registry.model.CacheUtils.newCacheBuilder;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaTm;
|
||||
|
||||
import com.github.benmanes.caffeine.cache.CacheLoader;
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/** Helpers for {@link BsaLabel}. */
|
||||
public final class BsaLabelUtils {
|
||||
|
||||
private BsaLabelUtils() {}
|
||||
|
||||
static final CacheLoader<VKey<BsaLabel>, Optional<BsaLabel>> CACHE_LOADER =
|
||||
new CacheLoader<VKey<BsaLabel>, Optional<BsaLabel>>() {
|
||||
|
||||
@Override
|
||||
public Optional<BsaLabel> load(VKey<BsaLabel> key) {
|
||||
return replicaTm().reTransact(() -> replicaTm().loadByKeyIfPresent(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<VKey<BsaLabel>, Optional<BsaLabel>> loadAll(
|
||||
Iterable<? extends VKey<BsaLabel>> keys) {
|
||||
// TODO(b/309173359): need this for DomainCheckFlow
|
||||
throw new UnsupportedOperationException(
|
||||
"LoadAll not supported by the BsaLabel cache loader.");
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A limited size, limited expiry cache of BSA labels.
|
||||
*
|
||||
* <p>BSA labels are used by the domain creation flow to verify that the requested domain name is
|
||||
* not blocked by the BSA program. Label caching is mainly a defense against two scenarios, the
|
||||
* initial rush and drop-catching, when clients run back-to-back domain creation requests around
|
||||
* the time when a domain becomes available.
|
||||
*
|
||||
* <p>Because of caching and the use of the replica database, new BSA labels installed in the
|
||||
* database will not take effect immediately. A blocked domain may be created due to race
|
||||
* condition. A `refresh` job will detect such domains and report them to BSA as unblockable
|
||||
* domains.
|
||||
*
|
||||
* <p>Since the cached BSA labels have the same usage pattern as the cached EppResources, the
|
||||
* cache configuration for the latter are reused here.
|
||||
*/
|
||||
private static LoadingCache<VKey<BsaLabel>, Optional<BsaLabel>> cacheBsaLabels =
|
||||
createBsaLabelsCache(getEppResourceCachingDuration());
|
||||
|
||||
private static LoadingCache<VKey<BsaLabel>, Optional<BsaLabel>> createBsaLabelsCache(
|
||||
Duration expiry) {
|
||||
return newCacheBuilder(expiry)
|
||||
.maximumSize(getEppResourceMaxCachedEntries())
|
||||
.build(CACHE_LOADER);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void clearCache() {
|
||||
cacheBsaLabels.invalidateAll();
|
||||
}
|
||||
|
||||
/** Checks if the {@code domainLabel} (the leading `part` of a domain name) is blocked by BSA. */
|
||||
public static boolean isLabelBlocked(String domainLabel) {
|
||||
return cacheBsaLabels.get(BsaLabel.vKey(domainLabel)).isPresent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
// Copyright 2023 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.bsa.persistence;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static google.registry.bsa.BsaStringUtils.DOMAIN_JOINER;
|
||||
import static google.registry.bsa.BsaStringUtils.DOMAIN_SPLITTER;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.bsa.api.UnblockableDomain;
|
||||
import google.registry.bsa.persistence.BsaUnblockableDomain.BsaUnblockableDomainId;
|
||||
import google.registry.model.CreateAutoTimestamp;
|
||||
import google.registry.persistence.VKey;
|
||||
import java.io.Serializable;
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.EnumType;
|
||||
import javax.persistence.Enumerated;
|
||||
import javax.persistence.Id;
|
||||
import javax.persistence.IdClass;
|
||||
|
||||
/** A domain matching a BSA label but is in use (registered or reserved), so cannot be blocked. */
|
||||
@Entity
|
||||
@IdClass(BsaUnblockableDomainId.class)
|
||||
class BsaUnblockableDomain {
|
||||
@Id String label;
|
||||
@Id String tld;
|
||||
|
||||
@Column(nullable = false)
|
||||
@Enumerated(EnumType.STRING)
|
||||
Reason reason;
|
||||
|
||||
/**
|
||||
* Creation time of this record, which is the most recent time when the domain was detected to be
|
||||
* in use wrt BSA. It may be during the processing of a download, or during some other job that
|
||||
* refreshes the state.
|
||||
*
|
||||
* <p>This field is for information only.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@Column(nullable = false)
|
||||
CreateAutoTimestamp createTime = CreateAutoTimestamp.create(null);
|
||||
|
||||
// For Hibernate
|
||||
BsaUnblockableDomain() {}
|
||||
|
||||
BsaUnblockableDomain(String label, String tld, Reason reason) {
|
||||
this.label = label;
|
||||
this.tld = tld;
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
String domainName() {
|
||||
return DOMAIN_JOINER.join(label, tld);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the equivalent {@link UnblockableDomain} instance, for use by communication with the
|
||||
* BSA API.
|
||||
*/
|
||||
UnblockableDomain toUnblockableDomain() {
|
||||
return UnblockableDomain.of(label, tld, UnblockableDomain.Reason.valueOf(reason.name()));
|
||||
}
|
||||
|
||||
VKey<BsaUnblockableDomain> toVkey() {
|
||||
return vKey(this.label, this.tld);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof BsaUnblockableDomain)) {
|
||||
return false;
|
||||
}
|
||||
BsaUnblockableDomain that = (BsaUnblockableDomain) o;
|
||||
return Objects.equal(label, that.label)
|
||||
&& Objects.equal(tld, that.tld)
|
||||
&& reason == that.reason
|
||||
&& Objects.equal(createTime, that.createTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(label, tld, reason, createTime);
|
||||
}
|
||||
|
||||
static BsaUnblockableDomain of(String domainName, Reason reason) {
|
||||
ImmutableList<String> parts = ImmutableList.copyOf(DOMAIN_SPLITTER.splitToList(domainName));
|
||||
verify(parts.size() == 2, "Invalid domain name: %s", domainName);
|
||||
return new BsaUnblockableDomain(parts.get(0), parts.get(1), reason);
|
||||
}
|
||||
|
||||
static VKey<BsaUnblockableDomain> vKey(String domainName) {
|
||||
ImmutableList<String> parts = ImmutableList.copyOf(DOMAIN_SPLITTER.splitToList(domainName));
|
||||
verify(parts.size() == 2, "Invalid domain name: %s", domainName);
|
||||
return vKey(parts.get(0), parts.get(1));
|
||||
}
|
||||
|
||||
static VKey<BsaUnblockableDomain> vKey(String label, String tld) {
|
||||
return VKey.create(BsaUnblockableDomain.class, new BsaUnblockableDomainId(label, tld));
|
||||
}
|
||||
|
||||
enum Reason {
|
||||
REGISTERED,
|
||||
RESERVED;
|
||||
}
|
||||
|
||||
static class BsaUnblockableDomainId implements Serializable {
|
||||
|
||||
private String label;
|
||||
private String tld;
|
||||
|
||||
@SuppressWarnings("unused") // For Hibernate
|
||||
BsaUnblockableDomainId() {}
|
||||
|
||||
BsaUnblockableDomainId(String label, String tld) {
|
||||
this.label = label;
|
||||
this.tld = tld;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof BsaUnblockableDomainId)) {
|
||||
return false;
|
||||
}
|
||||
BsaUnblockableDomainId that = (BsaUnblockableDomainId) o;
|
||||
return Objects.equal(label, that.label) && Objects.equal(tld, that.tld);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(label, tld);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
// Copyright 2023 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.bsa.persistence;
|
||||
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.ImmutableMap.toImmutableMap;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static google.registry.bsa.ReservedDomainsUtils.getAllReservedNames;
|
||||
import static google.registry.bsa.ReservedDomainsUtils.isReservedDomain;
|
||||
import static google.registry.bsa.persistence.Queries.queryLivesDomains;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static java.util.stream.Collectors.groupingBy;
|
||||
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Sets.SetView;
|
||||
import com.google.common.collect.Streams;
|
||||
import google.registry.bsa.BsaStringUtils;
|
||||
import google.registry.bsa.api.UnblockableDomain;
|
||||
import google.registry.bsa.api.UnblockableDomain.Reason;
|
||||
import google.registry.bsa.api.UnblockableDomainChange;
|
||||
import google.registry.model.ForeignKeyUtils;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.util.BatchedStreams;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
/**
|
||||
* Rechecks {@link BsaUnblockableDomain the registered/reserved domain names} in the database for
|
||||
* changes.
|
||||
*
|
||||
* <p>A registered/reserved domain name may change status in the following cases:
|
||||
*
|
||||
* <ul>
|
||||
* <li>A domain whose reason for being unblockable is `REGISTERED` will become blockable when the
|
||||
* domain is deregistered.
|
||||
* <li>A domain whose reason for being unblockable is `REGISTERED` will have its reason changed to
|
||||
* `RESERVED` if the domain is also on the reserved list.
|
||||
* <li>A domain whose reason for being unblockable is `RESERVED` will become blockable when the
|
||||
* domain is removed from the reserve list.
|
||||
* <li>A domain whose reason for being unblockable is `RESERVED` will have its reason changed to
|
||||
* `REGISTERED` if the domain is also on the reserved list.
|
||||
* <li>A blockable domain becomes unblockable when it is added to the reserve list.
|
||||
* <li>A blockable domain becomes unblockable when it is registered (with admin override).
|
||||
* </ul>
|
||||
*
|
||||
* <p>As a reminder, invalid domain names are not stored in the database. They change status only
|
||||
* when IDNs change in the TLDs, which rarely happens, and will be handled by dedicated procedures.
|
||||
*
|
||||
* <p>Domain blockability changes must be reported to BSA as follows:
|
||||
*
|
||||
* <ul>
|
||||
* <li>A blockable domain becoming unblockable: an addition
|
||||
* <li>An unblockable domain becoming blockable: a removal
|
||||
* <li>An unblockable domain with reason change: a removal followed by an insertion.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Since BSA has separate endpoints for receiving blockability changes, removals must be sent
|
||||
* before additions.
|
||||
*/
|
||||
public final class DomainsRefresher {
|
||||
|
||||
private final DateTime prevRefreshStartTime;
|
||||
private final int transactionBatchSize;
|
||||
private final DateTime now;
|
||||
|
||||
public DomainsRefresher(
|
||||
DateTime prevRefreshStartTime,
|
||||
DateTime now,
|
||||
Duration domainTxnMaxDuration,
|
||||
int transactionBatchSize) {
|
||||
this.prevRefreshStartTime = prevRefreshStartTime.minus(domainTxnMaxDuration);
|
||||
this.now = now;
|
||||
this.transactionBatchSize = transactionBatchSize;
|
||||
}
|
||||
|
||||
public ImmutableList<UnblockableDomainChange> checkForBlockabilityChanges() {
|
||||
ImmutableList<UnblockableDomainChange> downgrades = refreshStaleUnblockables();
|
||||
ImmutableList<UnblockableDomainChange> upgrades = getNewUnblockables();
|
||||
|
||||
ImmutableSet<String> upgradedDomains =
|
||||
upgrades.stream().map(UnblockableDomainChange::domainName).collect(toImmutableSet());
|
||||
ImmutableList<UnblockableDomainChange> trueDowngrades =
|
||||
downgrades.stream()
|
||||
.filter(c -> !upgradedDomains.contains(c.domainName()))
|
||||
.collect(toImmutableList());
|
||||
return new ImmutableList.Builder<UnblockableDomainChange>()
|
||||
.addAll(upgrades)
|
||||
.addAll(trueDowngrades)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all changes to unblockable domains that have been reported to BSA. Please see {@link
|
||||
* UnblockableDomainChange} for types of possible changes. Note that invalid domain names are not
|
||||
* covered by this class and will be handled separately.
|
||||
*
|
||||
* <p>The number of changes are expected to be small for now. It is limited by the number of
|
||||
* domain deregistrations and the number of names added or removed from the reserved lists since
|
||||
* the previous refresh.
|
||||
*/
|
||||
public ImmutableList<UnblockableDomainChange> refreshStaleUnblockables() {
|
||||
ImmutableList.Builder<UnblockableDomainChange> changes = new ImmutableList.Builder<>();
|
||||
ImmutableList<BsaUnblockableDomain> batch;
|
||||
Optional<BsaUnblockableDomain> lastRead = Optional.empty();
|
||||
do {
|
||||
batch = Queries.batchReadUnblockables(lastRead, transactionBatchSize);
|
||||
if (!batch.isEmpty()) {
|
||||
lastRead = Optional.of(batch.get(batch.size() - 1));
|
||||
changes.addAll(recheckStaleDomainsBatch(batch));
|
||||
}
|
||||
} while (batch.size() == transactionBatchSize);
|
||||
return changes.build();
|
||||
}
|
||||
|
||||
ImmutableSet<UnblockableDomainChange> recheckStaleDomainsBatch(
|
||||
ImmutableList<BsaUnblockableDomain> domains) {
|
||||
ImmutableMap<String, BsaUnblockableDomain> nameToEntity =
|
||||
domains.stream().collect(toImmutableMap(BsaUnblockableDomain::domainName, d -> d));
|
||||
|
||||
ImmutableSet<String> prevRegistered =
|
||||
domains.stream()
|
||||
.filter(d -> d.reason.equals(BsaUnblockableDomain.Reason.REGISTERED))
|
||||
.map(BsaUnblockableDomain::domainName)
|
||||
.collect(toImmutableSet());
|
||||
ImmutableSet<String> currRegistered =
|
||||
ImmutableSet.copyOf(
|
||||
ForeignKeyUtils.load(Domain.class, nameToEntity.keySet(), now).keySet());
|
||||
SetView<String> noLongerRegistered = Sets.difference(prevRegistered, currRegistered);
|
||||
SetView<String> newlyRegistered = Sets.difference(currRegistered, prevRegistered);
|
||||
|
||||
ImmutableSet<String> prevReserved =
|
||||
domains.stream()
|
||||
.filter(d -> d.reason.equals(BsaUnblockableDomain.Reason.RESERVED))
|
||||
.map(BsaUnblockableDomain::domainName)
|
||||
.collect(toImmutableSet());
|
||||
ImmutableSet<String> currReserved =
|
||||
nameToEntity.keySet().stream()
|
||||
.filter(domain -> isReservedDomain(domain, now))
|
||||
.collect(toImmutableSet());
|
||||
SetView<String> noLongerReserved = Sets.difference(prevReserved, currReserved);
|
||||
|
||||
ImmutableSet.Builder<UnblockableDomainChange> changes = new ImmutableSet.Builder<>();
|
||||
// Newly registered: reserved -> registered
|
||||
for (String domainName : newlyRegistered) {
|
||||
BsaUnblockableDomain domain = nameToEntity.get(domainName);
|
||||
UnblockableDomain unblockable =
|
||||
UnblockableDomain.of(domain.label, domain.tld, Reason.valueOf(domain.reason.name()));
|
||||
changes.add(UnblockableDomainChange.ofChanged(unblockable, Reason.REGISTERED));
|
||||
}
|
||||
// No longer registered: registered -> reserved/NONE
|
||||
for (String domainName : noLongerRegistered) {
|
||||
BsaUnblockableDomain domain = nameToEntity.get(domainName);
|
||||
UnblockableDomain unblockable =
|
||||
UnblockableDomain.of(domain.label, domain.tld, Reason.valueOf(domain.reason.name()));
|
||||
changes.add(
|
||||
currReserved.contains(domainName)
|
||||
? UnblockableDomainChange.ofChanged(unblockable, Reason.RESERVED)
|
||||
: UnblockableDomainChange.ofDeleted(unblockable));
|
||||
}
|
||||
// No longer reserved: reserved -> registered/None (the former duplicates with newly-registered)
|
||||
for (String domainName : noLongerReserved) {
|
||||
BsaUnblockableDomain domain = nameToEntity.get(domainName);
|
||||
UnblockableDomain unblockable =
|
||||
UnblockableDomain.of(domain.label, domain.tld, Reason.valueOf(domain.reason.name()));
|
||||
if (!currRegistered.contains(domainName)) {
|
||||
changes.add(UnblockableDomainChange.ofDeleted(unblockable));
|
||||
}
|
||||
}
|
||||
return changes.build();
|
||||
}
|
||||
|
||||
public ImmutableList<UnblockableDomainChange> getNewUnblockables() {
|
||||
ImmutableSet<String> newCreated = getNewlyCreatedUnblockables(prevRefreshStartTime, now);
|
||||
ImmutableSet<String> newReserved = getNewlyReservedUnblockables(now, transactionBatchSize);
|
||||
SetView<String> reservedNotCreated = Sets.difference(newReserved, newCreated);
|
||||
return Streams.concat(
|
||||
newCreated.stream()
|
||||
.map(name -> UnblockableDomain.of(name, Reason.REGISTERED))
|
||||
.map(UnblockableDomainChange::ofNew),
|
||||
reservedNotCreated.stream()
|
||||
.map(name -> UnblockableDomain.of(name, Reason.RESERVED))
|
||||
.map(UnblockableDomainChange::ofNew))
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
static ImmutableSet<String> getNewlyCreatedUnblockables(
|
||||
DateTime prevRefreshStartTime, DateTime now) {
|
||||
ImmutableSet<String> liveDomains = queryLivesDomains(prevRefreshStartTime, now);
|
||||
return getUnblockedDomainNames(liveDomains);
|
||||
}
|
||||
|
||||
static ImmutableSet<String> getNewlyReservedUnblockables(DateTime now, int batchSize) {
|
||||
Stream<String> allReserved = getAllReservedNames(now);
|
||||
return BatchedStreams.toBatches(allReserved, batchSize)
|
||||
.map(DomainsRefresher::getUnblockedDomainNames)
|
||||
.flatMap(ImmutableSet::stream)
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
|
||||
static ImmutableSet<String> getUnblockedDomainNames(ImmutableCollection<String> domainNames) {
|
||||
Map<String, List<String>> labelToNames =
|
||||
domainNames.stream().collect(groupingBy(BsaStringUtils::getLabelInDomain));
|
||||
ImmutableSet<String> bsaLabels =
|
||||
Queries.queryBsaLabelByLabels(ImmutableSet.copyOf(labelToNames.keySet()))
|
||||
.map(BsaLabel::getLabel)
|
||||
.collect(toImmutableSet());
|
||||
return labelToNames.entrySet().stream()
|
||||
.filter(entry -> !bsaLabels.contains(entry.getKey()))
|
||||
.map(Entry::getValue)
|
||||
.flatMap(List::stream)
|
||||
.collect(toImmutableSet());
|
||||
}
|
||||
|
||||
public void applyUnblockableChanges(ImmutableList<UnblockableDomainChange> changes) {
|
||||
ImmutableMap<String, ImmutableSet<UnblockableDomainChange>> changesByType =
|
||||
ImmutableMap.copyOf(
|
||||
changes.stream()
|
||||
.collect(
|
||||
groupingBy(
|
||||
change -> change.isDelete() ? "remove" : "change", toImmutableSet())));
|
||||
tm().transact(
|
||||
() -> {
|
||||
if (changesByType.containsKey("remove")) {
|
||||
tm().delete(
|
||||
changesByType.get("remove").stream()
|
||||
.map(c -> BsaUnblockableDomain.vKey(c.domainName()))
|
||||
.collect(toImmutableSet()));
|
||||
}
|
||||
if (changesByType.containsKey("change")) {
|
||||
tm().putAll(
|
||||
changesByType.get("change").stream()
|
||||
.map(UnblockableDomainChange::newValue)
|
||||
.collect(toImmutableSet()));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,19 @@
|
||||
|
||||
package google.registry.bsa.persistence;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static google.registry.bsa.DownloadStage.CHECKSUMS_DO_NOT_MATCH;
|
||||
import static google.registry.bsa.DownloadStage.MAKE_ORDER_AND_LABEL_DIFF;
|
||||
import static google.registry.bsa.DownloadStage.NOP;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.bsa.BlockList;
|
||||
import google.registry.bsa.BlockListType;
|
||||
import google.registry.bsa.DownloadStage;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Information needed when handling a download from BSA. */
|
||||
@AutoValue
|
||||
@@ -26,6 +34,8 @@ public abstract class DownloadSchedule {
|
||||
|
||||
abstract long jobId();
|
||||
|
||||
abstract DateTime jobCreationTime();
|
||||
|
||||
public abstract String jobName();
|
||||
|
||||
public abstract DownloadStage stage();
|
||||
@@ -37,11 +47,57 @@ public abstract class DownloadSchedule {
|
||||
* Returns true if download should be processed even if the checksums show that it has not changed
|
||||
* from the previous one.
|
||||
*/
|
||||
abstract boolean alwaysDownload();
|
||||
public abstract boolean alwaysDownload();
|
||||
|
||||
/** Updates the current job to the new stage. */
|
||||
public void updateJobStage(DownloadStage stage) {
|
||||
tm().transact(
|
||||
() -> {
|
||||
BsaDownload bsaDownload = tm().loadByKey(BsaDownload.vKey(jobId()));
|
||||
verify(
|
||||
stage.compareTo(bsaDownload.getStage()) > 0,
|
||||
"Invalid new stage [%s]. Must move forward from [%s]",
|
||||
bsaDownload.getStage(),
|
||||
stage);
|
||||
bsaDownload.setStage(stage);
|
||||
tm().put(bsaDownload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current job to the new stage and sets the checksums of the downloaded files.
|
||||
*
|
||||
* <p>This method may only be invoked during the {@code DOWNLOAD} stage, and the target stage must
|
||||
* be one of {@code MAKE_DIFF}, {@code CHECK_FOR_STALE_UNBLOCKABLES}, {@code NOP}, or {@code
|
||||
* CHECKSUMS_NOT_MATCH}.
|
||||
*/
|
||||
public DownloadSchedule updateJobStage(
|
||||
DownloadStage stage, ImmutableMap<BlockListType, String> checksums) {
|
||||
checkArgument(
|
||||
stage.equals(MAKE_ORDER_AND_LABEL_DIFF)
|
||||
|| stage.equals(NOP)
|
||||
|| stage.equals(CHECKSUMS_DO_NOT_MATCH),
|
||||
"Invalid stage [%s]",
|
||||
stage);
|
||||
return tm().transact(
|
||||
() -> {
|
||||
BsaDownload bsaDownload = tm().loadByKey(BsaDownload.vKey(jobId()));
|
||||
verify(
|
||||
bsaDownload.getStage().equals(DownloadStage.DOWNLOAD_BLOCK_LISTS),
|
||||
"Invalid invocation. May only invoke during the DOWNLOAD stage.",
|
||||
bsaDownload.getStage(),
|
||||
stage);
|
||||
bsaDownload.setStage(stage);
|
||||
bsaDownload.setChecksums(checksums);
|
||||
tm().put(bsaDownload);
|
||||
return of(bsaDownload);
|
||||
});
|
||||
}
|
||||
|
||||
static DownloadSchedule of(BsaDownload currentJob) {
|
||||
return new AutoValue_DownloadSchedule(
|
||||
currentJob.getJobId(),
|
||||
currentJob.getCreationTime(),
|
||||
currentJob.getJobName(),
|
||||
currentJob.getStage(),
|
||||
Optional.empty(),
|
||||
@@ -52,6 +108,7 @@ public abstract class DownloadSchedule {
|
||||
BsaDownload currentJob, CompletedJob latestCompleted, boolean alwaysDownload) {
|
||||
return new AutoValue_DownloadSchedule(
|
||||
currentJob.getJobId(),
|
||||
currentJob.getCreationTime(),
|
||||
currentJob.getJobName(),
|
||||
currentJob.getStage(),
|
||||
Optional.of(latestCompleted),
|
||||
@@ -63,7 +120,7 @@ public abstract class DownloadSchedule {
|
||||
public abstract static class CompletedJob {
|
||||
public abstract String jobName();
|
||||
|
||||
public abstract ImmutableMap<BlockList, String> checksums();
|
||||
public abstract ImmutableMap<BlockListType, String> checksums();
|
||||
|
||||
static CompletedJob of(BsaDownload completedJob) {
|
||||
return new AutoValue_DownloadSchedule_CompletedJob(
|
||||
|
||||
@@ -15,18 +15,22 @@
|
||||
package google.registry.bsa.persistence;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static google.registry.bsa.DownloadStage.CHECKSUMS_NOT_MATCH;
|
||||
import static google.registry.bsa.DownloadStage.CHECKSUMS_DO_NOT_MATCH;
|
||||
import static google.registry.bsa.DownloadStage.DONE;
|
||||
import static google.registry.bsa.DownloadStage.NOP;
|
||||
import static google.registry.bsa.persistence.RefreshScheduler.fetchMostRecentRefresh;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static org.joda.time.Duration.standardSeconds;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import google.registry.bsa.persistence.DownloadSchedule.CompletedJob;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.util.Clock;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
/**
|
||||
@@ -61,7 +65,10 @@ public final class DownloadScheduler {
|
||||
private final Clock clock;
|
||||
|
||||
@Inject
|
||||
DownloadScheduler(Duration downloadInterval, Duration maxNopInterval, Clock clock) {
|
||||
DownloadScheduler(
|
||||
@Config("bsaDownloadInterval") Duration downloadInterval,
|
||||
@Config("bsaMaxNopInterval") Duration maxNopInterval,
|
||||
Clock clock) {
|
||||
this.downloadInterval = downloadInterval;
|
||||
this.maxNopInterval = maxNopInterval;
|
||||
this.clock = clock;
|
||||
@@ -71,26 +78,33 @@ public final class DownloadScheduler {
|
||||
* Returns a {@link DownloadSchedule} instance that describes the work to be performed by an
|
||||
* invocation of the download action, if applicable; or {@link Optional#empty} when there is
|
||||
* nothing to do.
|
||||
*
|
||||
* <p>For an interrupted job, work will resume from the {@link DownloadSchedule#stage}.
|
||||
*/
|
||||
public Optional<DownloadSchedule> schedule() {
|
||||
return tm().transact(
|
||||
() -> {
|
||||
ImmutableList<BsaDownload> recentJobs = loadRecentProcessedJobs();
|
||||
if (recentJobs.isEmpty()) {
|
||||
// No jobs initiated ever.
|
||||
ImmutableList<BsaDownload> recentDownloads = fetchTwoMostRecentDownloads();
|
||||
Optional<BsaDomainRefresh> mostRecentRefresh = fetchMostRecentRefresh();
|
||||
if (mostRecentRefresh.isPresent() && !mostRecentRefresh.get().isDone()) {
|
||||
// Ongoing refresh. Wait it out.
|
||||
return Optional.empty();
|
||||
}
|
||||
if (recentDownloads.isEmpty()) {
|
||||
// No downloads initiated ever.
|
||||
return Optional.of(scheduleNewJob(Optional.empty()));
|
||||
}
|
||||
BsaDownload mostRecent = recentJobs.get(0);
|
||||
BsaDownload mostRecent = recentDownloads.get(0);
|
||||
if (mostRecent.getStage().equals(DONE)) {
|
||||
return isTimeAgain(mostRecent, downloadInterval)
|
||||
? Optional.of(scheduleNewJob(Optional.of(mostRecent)))
|
||||
: Optional.empty();
|
||||
} else if (recentJobs.size() == 1) {
|
||||
} else if (recentDownloads.size() == 1) {
|
||||
// First job ever, still in progress
|
||||
return Optional.of(DownloadSchedule.of(recentJobs.get(0)));
|
||||
return Optional.of(DownloadSchedule.of(recentDownloads.get(0)));
|
||||
} else {
|
||||
// Job in progress, with completed previous jobs.
|
||||
BsaDownload prev = recentJobs.get(1);
|
||||
BsaDownload prev = recentDownloads.get(1);
|
||||
verify(prev.getStage().equals(DONE), "Unexpectedly found two ongoing jobs.");
|
||||
return Optional.of(
|
||||
DownloadSchedule.of(
|
||||
@@ -101,6 +115,16 @@ public final class DownloadScheduler {
|
||||
});
|
||||
}
|
||||
|
||||
Optional<DateTime> latestCompletedJobTime() {
|
||||
return tm().transact(
|
||||
() -> {
|
||||
return fetchTwoMostRecentDownloads().stream()
|
||||
.filter(job -> Objects.equals(job.getStage(), DONE))
|
||||
.map(BsaDownload::getCreationTime)
|
||||
.findFirst();
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isTimeAgain(BsaDownload mostRecent, Duration interval) {
|
||||
return mostRecent.getCreationTime().plus(interval).minus(CRON_JITTER).isBefore(clock.nowUtc());
|
||||
}
|
||||
@@ -118,14 +142,25 @@ public final class DownloadScheduler {
|
||||
.orElseGet(() -> DownloadSchedule.of(job));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches up to two most recent downloads, ordered by time in descending order. The first one may
|
||||
* be ongoing, and the second one (if exists) must be completed.
|
||||
*
|
||||
* <p>Jobs that do not download the data are ignored.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
ImmutableList<BsaDownload> loadRecentProcessedJobs() {
|
||||
static ImmutableList<BsaDownload> fetchTwoMostRecentDownloads() {
|
||||
return ImmutableList.copyOf(
|
||||
tm().getEntityManager()
|
||||
.createQuery(
|
||||
"FROM BsaDownload WHERE stage NOT IN :nop_stages ORDER BY creationTime DESC")
|
||||
.setParameter("nop_stages", ImmutableList.of(CHECKSUMS_NOT_MATCH, NOP))
|
||||
"FROM BsaDownload WHERE stage NOT IN :nop_stages ORDER BY creationTime DESC",
|
||||
BsaDownload.class)
|
||||
.setParameter("nop_stages", ImmutableList.of(CHECKSUMS_DO_NOT_MATCH, NOP))
|
||||
.setMaxResults(2)
|
||||
.getResultList());
|
||||
}
|
||||
|
||||
static Optional<BsaDownload> fetchMostRecentDownload() {
|
||||
return fetchTwoMostRecentDownloads().stream().findFirst();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright 2023 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.bsa.persistence;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.Sets.difference;
|
||||
import static google.registry.bsa.ReservedDomainsUtils.isReservedDomain;
|
||||
import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static java.util.stream.Collectors.groupingBy;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.flogger.LazyArgs;
|
||||
import google.registry.bsa.IdnChecker;
|
||||
import google.registry.bsa.api.BlockLabel;
|
||||
import google.registry.bsa.api.BlockLabel.LabelType;
|
||||
import google.registry.bsa.api.UnblockableDomain;
|
||||
import google.registry.bsa.api.UnblockableDomain.Reason;
|
||||
import google.registry.model.ForeignKeyUtils;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.tld.Tld;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Applies the BSA label diffs from the latest BSA download. */
|
||||
public final class LabelDiffUpdates {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
private static final Joiner DOMAIN_JOINER = Joiner.on('.');
|
||||
|
||||
private LabelDiffUpdates() {}
|
||||
|
||||
/**
|
||||
* Applies the label diffs to the database and collects matching domains that are in use
|
||||
* (registered or reserved) for reporting.
|
||||
*
|
||||
* @return A collection of domains in use
|
||||
*/
|
||||
public static ImmutableList<UnblockableDomain> applyLabelDiff(
|
||||
ImmutableList<BlockLabel> labels,
|
||||
IdnChecker idnChecker,
|
||||
DownloadSchedule schedule,
|
||||
DateTime now) {
|
||||
ImmutableList.Builder<UnblockableDomain> nonBlockedDomains = new ImmutableList.Builder<>();
|
||||
ImmutableMap<LabelType, ImmutableList<BlockLabel>> labelsByType =
|
||||
ImmutableMap.copyOf(
|
||||
labels.stream().collect(groupingBy(BlockLabel::labelType, toImmutableList())));
|
||||
|
||||
tm().transact(
|
||||
() -> {
|
||||
for (Map.Entry<LabelType, ImmutableList<BlockLabel>> entry :
|
||||
labelsByType.entrySet()) {
|
||||
switch (entry.getKey()) {
|
||||
case CREATE:
|
||||
// With current Cloud SQL, label upsert throughput is about 200/second. If
|
||||
// better performance is needed, consider bulk insert in native SQL.
|
||||
tm().putAll(
|
||||
entry.getValue().stream()
|
||||
.filter(label -> isValidInAtLeastOneTld(label, idnChecker))
|
||||
.map(
|
||||
label ->
|
||||
new BsaLabel(label.label(), schedule.jobCreationTime()))
|
||||
.collect(toImmutableList()));
|
||||
// May not find all unblockables due to race condition: DomainCreateFlow uses
|
||||
// cached BsaLabels. Eventually will be consistent.
|
||||
nonBlockedDomains.addAll(
|
||||
tallyUnblockableDomainsForNewLabels(entry.getValue(), idnChecker, now));
|
||||
break;
|
||||
case DELETE:
|
||||
ImmutableSet<String> deletedLabels =
|
||||
entry.getValue().stream()
|
||||
.filter(label -> isValidInAtLeastOneTld(label, idnChecker))
|
||||
.map(BlockLabel::label)
|
||||
.collect(toImmutableSet());
|
||||
// Delete labels in DB. Also cascade-delete BsaUnblockableDomain.
|
||||
int nDeleted = Queries.deleteBsaLabelByLabels(deletedLabels);
|
||||
if (nDeleted != deletedLabels.size()) {
|
||||
logger.atSevere().log(
|
||||
"Only found %s entities among the %s labels: [%s]",
|
||||
nDeleted, deletedLabels.size(), deletedLabels);
|
||||
}
|
||||
break;
|
||||
case NEW_ORDER_ASSOCIATION:
|
||||
ImmutableSet<String> affectedLabels =
|
||||
entry.getValue().stream()
|
||||
.filter(label -> isValidInAtLeastOneTld(label, idnChecker))
|
||||
.map(BlockLabel::label)
|
||||
.collect(toImmutableSet());
|
||||
ImmutableSet<String> labelsInDb =
|
||||
Queries.queryBsaLabelByLabels(affectedLabels)
|
||||
.map(BsaLabel::getLabel)
|
||||
.collect(toImmutableSet());
|
||||
verify(
|
||||
labelsInDb.size() == affectedLabels.size(),
|
||||
"Missing labels in DB: %s",
|
||||
LazyArgs.lazy(() -> difference(affectedLabels, labelsInDb)));
|
||||
|
||||
// Reuse registered and reserved names that are already computed.
|
||||
Queries.queryBsaUnblockableDomainByLabels(affectedLabels)
|
||||
.map(BsaUnblockableDomain::toUnblockableDomain)
|
||||
.forEach(nonBlockedDomains::add);
|
||||
|
||||
for (BlockLabel label : entry.getValue()) {
|
||||
getInvalidTldsForLabel(label, idnChecker)
|
||||
.map(tld -> UnblockableDomain.of(label.label(), tld, Reason.INVALID))
|
||||
.forEach(nonBlockedDomains::add);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
TRANSACTION_REPEATABLE_READ);
|
||||
logger.atInfo().log("Processed %s of labels.", labels.size());
|
||||
return nonBlockedDomains.build();
|
||||
}
|
||||
|
||||
static ImmutableList<UnblockableDomain> tallyUnblockableDomainsForNewLabels(
|
||||
ImmutableList<BlockLabel> labels, IdnChecker idnChecker, DateTime now) {
|
||||
ImmutableList.Builder<UnblockableDomain> nonBlockedDomains = new ImmutableList.Builder<>();
|
||||
|
||||
for (BlockLabel label : labels) {
|
||||
getInvalidTldsForLabel(label, idnChecker)
|
||||
.map(tld -> UnblockableDomain.of(label.label(), tld, Reason.INVALID))
|
||||
.forEach(nonBlockedDomains::add);
|
||||
}
|
||||
|
||||
ImmutableSet<String> validDomainNames =
|
||||
labels.stream()
|
||||
.map(label -> validDomainNamesForLabel(label, idnChecker))
|
||||
.flatMap(x -> x)
|
||||
.collect(toImmutableSet());
|
||||
ImmutableSet<String> registeredDomainNames =
|
||||
ImmutableSet.copyOf(ForeignKeyUtils.load(Domain.class, validDomainNames, now).keySet());
|
||||
for (String domain : registeredDomainNames) {
|
||||
nonBlockedDomains.add(UnblockableDomain.of(domain, Reason.REGISTERED));
|
||||
tm().put(BsaUnblockableDomain.of(domain, BsaUnblockableDomain.Reason.REGISTERED));
|
||||
}
|
||||
|
||||
ImmutableSet<String> reservedDomainNames =
|
||||
difference(validDomainNames, registeredDomainNames).stream()
|
||||
.filter(domain -> isReservedDomain(domain, now))
|
||||
.collect(toImmutableSet());
|
||||
for (String domain : reservedDomainNames) {
|
||||
nonBlockedDomains.add(UnblockableDomain.of(domain, Reason.RESERVED));
|
||||
tm().put(BsaUnblockableDomain.of(domain, BsaUnblockableDomain.Reason.RESERVED));
|
||||
}
|
||||
return nonBlockedDomains.build();
|
||||
}
|
||||
|
||||
static Stream<String> validDomainNamesForLabel(BlockLabel label, IdnChecker idnChecker) {
|
||||
return getValidTldsForLabel(label, idnChecker)
|
||||
.map(tld -> DOMAIN_JOINER.join(label.label(), tld));
|
||||
}
|
||||
|
||||
static Stream<String> getInvalidTldsForLabel(BlockLabel label, IdnChecker idnChecker) {
|
||||
return idnChecker.getForbiddingTlds(label.idnTables()).stream().map(Tld::getTldStr);
|
||||
}
|
||||
|
||||
static Stream<String> getValidTldsForLabel(BlockLabel label, IdnChecker idnChecker) {
|
||||
return idnChecker.getSupportingTlds(label.idnTables()).stream().map(Tld::getTldStr);
|
||||
}
|
||||
|
||||
static boolean isValidInAtLeastOneTld(BlockLabel label, IdnChecker idnChecker) {
|
||||
return getValidTldsForLabel(label, idnChecker).findAny().isPresent();
|
||||
}
|
||||
}
|
||||
112
core/src/main/java/google/registry/bsa/persistence/Queries.java
Normal file
112
core/src/main/java/google/registry/bsa/persistence/Queries.java
Normal file
@@ -0,0 +1,112 @@
|
||||
// Copyright 2023 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.bsa.persistence;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static google.registry.bsa.BsaStringUtils.DOMAIN_SPLITTER;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.collect.ImmutableCollection;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.model.CreateAutoTimestamp;
|
||||
import google.registry.model.ForeignKeyUtils;
|
||||
import google.registry.model.domain.Domain;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Helpers for querying BSA JPA entities. */
|
||||
class Queries {
|
||||
|
||||
private Queries() {}
|
||||
|
||||
private static Object detach(Object obj) {
|
||||
tm().getEntityManager().detach(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
static Stream<BsaUnblockableDomain> queryBsaUnblockableDomainByLabels(
|
||||
ImmutableCollection<String> labels) {
|
||||
return ((Stream<?>)
|
||||
tm().getEntityManager()
|
||||
.createQuery("FROM BsaUnblockableDomain WHERE label in (:labels)")
|
||||
.setParameter("labels", labels)
|
||||
.getResultStream())
|
||||
.map(Queries::detach)
|
||||
.map(BsaUnblockableDomain.class::cast);
|
||||
}
|
||||
|
||||
static Stream<BsaLabel> queryBsaLabelByLabels(ImmutableCollection<String> labels) {
|
||||
return ((Stream<?>)
|
||||
tm().getEntityManager()
|
||||
.createQuery("FROM BsaLabel where label in (:labels)")
|
||||
.setParameter("labels", labels)
|
||||
.getResultStream())
|
||||
.map(Queries::detach)
|
||||
.map(BsaLabel.class::cast);
|
||||
}
|
||||
|
||||
static int deleteBsaLabelByLabels(ImmutableCollection<String> labels) {
|
||||
return tm().getEntityManager()
|
||||
.createQuery("DELETE FROM BsaLabel where label IN (:deleted_labels)")
|
||||
.setParameter("deleted_labels", labels)
|
||||
.executeUpdate();
|
||||
}
|
||||
|
||||
static ImmutableList<BsaUnblockableDomain> batchReadUnblockables(
|
||||
Optional<BsaUnblockableDomain> lastRead, int batchSize) {
|
||||
return ImmutableList.copyOf(
|
||||
tm().getEntityManager()
|
||||
.createQuery(
|
||||
"FROM BsaUnblockableDomain d WHERE d.label > :label OR (d.label = :label AND d.tld"
|
||||
+ " > :tld) ORDER BY d.tld, d.label ")
|
||||
.setParameter("label", lastRead.map(d -> d.label).orElse(""))
|
||||
.setParameter("tld", lastRead.map(d -> d.tld).orElse(""))
|
||||
.setMaxResults(batchSize)
|
||||
.getResultList());
|
||||
}
|
||||
|
||||
static ImmutableSet<String> queryUnblockablesByNames(ImmutableSet<String> domains) {
|
||||
String labelTldParis =
|
||||
domains.stream()
|
||||
.map(
|
||||
domain -> {
|
||||
List<String> parts = DOMAIN_SPLITTER.splitToList(domain);
|
||||
verify(parts.size() == 2, "Invalid domain name %s", domain);
|
||||
return String.format("('%s','%s')", parts.get(0), parts.get(1));
|
||||
})
|
||||
.collect(Collectors.joining(","));
|
||||
String sql =
|
||||
String.format(
|
||||
"SELECT CONCAT(d.label, '.', d.tld) FROM \"BsaUnblockableDomain\" d "
|
||||
+ "WHERE (d.label, d.tld) IN (%s)",
|
||||
labelTldParis);
|
||||
return ImmutableSet.copyOf(tm().getEntityManager().createNativeQuery(sql).getResultList());
|
||||
}
|
||||
|
||||
static ImmutableSet<String> queryLivesDomains(DateTime minCreationTime, DateTime now) {
|
||||
ImmutableSet<String> candidates =
|
||||
ImmutableSet.copyOf(
|
||||
tm().getEntityManager()
|
||||
.createQuery(
|
||||
"SELECT domainName FROM Domain WHERE creationTime >= :time ", String.class)
|
||||
.setParameter("time", CreateAutoTimestamp.create(minCreationTime))
|
||||
.getResultList());
|
||||
return ImmutableSet.copyOf(ForeignKeyUtils.load(Domain.class, candidates, now).keySet());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2023 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.bsa.persistence;
|
||||
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import google.registry.bsa.RefreshStage;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Information needed when handling a domain refresh. */
|
||||
@AutoValue
|
||||
public abstract class RefreshSchedule {
|
||||
|
||||
abstract long jobId();
|
||||
|
||||
abstract DateTime jobCreationTime();
|
||||
|
||||
public abstract String jobName();
|
||||
|
||||
public abstract RefreshStage stage();
|
||||
|
||||
/** The most recent job that ended in the {@code DONE} stage. */
|
||||
public abstract DateTime prevRefreshTime();
|
||||
|
||||
/** Updates the current job to the new stage. */
|
||||
@CanIgnoreReturnValue
|
||||
public RefreshSchedule updateJobStage(RefreshStage stage) {
|
||||
return tm().transact(
|
||||
() -> {
|
||||
BsaDomainRefresh bsaRefresh = tm().loadByKey(BsaDomainRefresh.vKey(jobId()));
|
||||
verify(
|
||||
stage.compareTo(bsaRefresh.getStage()) > 0,
|
||||
"Invalid new stage [%s]. Must move forward from [%s]",
|
||||
bsaRefresh.getStage(),
|
||||
stage);
|
||||
bsaRefresh.setStage(stage);
|
||||
tm().put(bsaRefresh);
|
||||
return of(bsaRefresh, prevRefreshTime());
|
||||
});
|
||||
}
|
||||
|
||||
static RefreshSchedule of(BsaDomainRefresh job, DateTime prevJobCreationTime) {
|
||||
return new AutoValue_RefreshSchedule(
|
||||
job.getJobId(),
|
||||
job.getCreationTime(),
|
||||
job.getJobName(),
|
||||
job.getStage(),
|
||||
prevJobCreationTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Copyright 2023 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.bsa.persistence;
|
||||
|
||||
import static google.registry.bsa.persistence.DownloadScheduler.fetchMostRecentDownload;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Assigns work for each cron invocation of domain refresh job. */
|
||||
public class RefreshScheduler {
|
||||
|
||||
@Inject
|
||||
RefreshScheduler() {}
|
||||
|
||||
public Optional<RefreshSchedule> schedule() {
|
||||
return tm().transact(
|
||||
() -> {
|
||||
ImmutableList<BsaDomainRefresh> recentJobs = fetchMostRecentRefreshes();
|
||||
Optional<BsaDownload> mostRecentDownload = fetchMostRecentDownload();
|
||||
if (mostRecentDownload.isPresent() && !mostRecentDownload.get().isDone()) {
|
||||
// Ongoing download exists. Must wait it out.
|
||||
return Optional.empty();
|
||||
}
|
||||
if (recentJobs.size() > 1) {
|
||||
BsaDomainRefresh mostRecent = recentJobs.get(0);
|
||||
if (mostRecent.isDone()) {
|
||||
return Optional.of(scheduleNewJob(mostRecent.getCreationTime()));
|
||||
} else {
|
||||
return Optional.of(
|
||||
rescheduleOngoingJob(mostRecent, recentJobs.get(1).getCreationTime()));
|
||||
}
|
||||
}
|
||||
if (recentJobs.size() == 1 && recentJobs.get(0).isDone()) {
|
||||
return Optional.of(scheduleNewJob(recentJobs.get(0).getCreationTime()));
|
||||
}
|
||||
// No previously completed refreshes. Need start time of a completed download as
|
||||
// lower bound of refresh checks.
|
||||
if (!mostRecentDownload.isPresent()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
DateTime prevDownloadTime = mostRecentDownload.get().getCreationTime();
|
||||
if (recentJobs.isEmpty()) {
|
||||
return Optional.of(scheduleNewJob(prevDownloadTime));
|
||||
} else {
|
||||
return Optional.of(rescheduleOngoingJob(recentJobs.get(0), prevDownloadTime));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RefreshSchedule scheduleNewJob(DateTime prevRefreshTime) {
|
||||
BsaDomainRefresh newJob = new BsaDomainRefresh();
|
||||
tm().insert(newJob);
|
||||
return RefreshSchedule.of(newJob, prevRefreshTime);
|
||||
}
|
||||
|
||||
RefreshSchedule rescheduleOngoingJob(BsaDomainRefresh ongoingJob, DateTime prevJobStartTime) {
|
||||
return RefreshSchedule.of(ongoingJob, prevJobStartTime);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static ImmutableList<BsaDomainRefresh> fetchMostRecentRefreshes() {
|
||||
return ImmutableList.copyOf(
|
||||
tm().getEntityManager()
|
||||
.createQuery("FROM BsaDomainRefresh ORDER BY creationTime DESC", BsaDomainRefresh.class)
|
||||
.setMaxResults(2)
|
||||
.getResultList());
|
||||
}
|
||||
|
||||
static Optional<BsaDomainRefresh> fetchMostRecentRefresh() {
|
||||
return fetchMostRecentRefreshes().stream().findFirst();
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ import dagger.Provides;
|
||||
import google.registry.dns.ReadDnsRefreshRequestsAction;
|
||||
import google.registry.model.common.DnsRefreshRequest;
|
||||
import google.registry.persistence.transaction.JpaTransactionManager;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import google.registry.util.YamlUtils;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
@@ -1043,6 +1044,17 @@ public final class RegistryConfig {
|
||||
return config.registryPolicy.whoisDisclaimer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Message template for whois response when queried domain is blocked by BSA.
|
||||
*
|
||||
* @see google.registry.whois.WhoisResponse
|
||||
*/
|
||||
@Provides
|
||||
@Config("domainBlockedByBsaTemplate")
|
||||
public static String provideDomainBlockedByBsaTemplate(RegistryConfigSettings config) {
|
||||
return config.registryPolicy.domainBlockedByBsaTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum QPS for the Google Cloud Monitoring V3 (aka Stackdriver) API. The QPS limit can be
|
||||
* adjusted by contacting Cloud Support.
|
||||
@@ -1433,9 +1445,15 @@ public final class RegistryConfig {
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("bsaLabelTxnBatchSize")
|
||||
public static int provideBsaLabelTxnBatchSize(RegistryConfigSettings config) {
|
||||
return config.bsa.bsaLabelTxnBatchSize;
|
||||
@Config("bsaTxnBatchSize")
|
||||
public static int provideBsaTxnBatchSize(RegistryConfigSettings config) {
|
||||
return config.bsa.bsaTxnBatchSize;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("domainTxnMaxDuration")
|
||||
public static Duration provideDomainTxnMaxDuration(RegistryConfigSettings config) {
|
||||
return Duration.standardSeconds(config.bsa.domainTxnMaxDurationSeconds);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -105,6 +105,7 @@ public class RegistryConfigSettings {
|
||||
public String reservedTermsExportDisclaimer;
|
||||
public String whoisRedactedEmailText;
|
||||
public String whoisDisclaimer;
|
||||
public String domainBlockedByBsaTemplate;
|
||||
public String rdapTos;
|
||||
public String rdapTosStaticUrl;
|
||||
public String registryName;
|
||||
@@ -271,7 +272,8 @@ public class RegistryConfigSettings {
|
||||
public int bsaLockLeaseExpiryMinutes;
|
||||
public int bsaDownloadIntervalMinutes;
|
||||
public int bsaMaxNopIntervalHours;
|
||||
public int bsaLabelTxnBatchSize;
|
||||
public int bsaTxnBatchSize;
|
||||
public int domainTxnMaxDurationSeconds;
|
||||
public String authUrl;
|
||||
public int authTokenExpirySeconds;
|
||||
public Map<String, String> dataUrls;
|
||||
|
||||
@@ -130,6 +130,12 @@ registryPolicy:
|
||||
unlawful behavior. We reserve the right to restrict or deny your access to
|
||||
the WHOIS database, and may modify these terms at any time.
|
||||
|
||||
# BSA blocked domain name template.
|
||||
domainBlockedByBsaTemplate: |
|
||||
Domain Name: %s
|
||||
>>> This name is not available for registration.
|
||||
>>> This name has been blocked by a GlobalBlock service.
|
||||
|
||||
# RDAP Terms of Service text displayed at the /rdap/help/tos endpoint.
|
||||
rdapTos: >
|
||||
By querying our Domain Database as part of the RDAP pilot program (RDAP
|
||||
@@ -609,6 +615,22 @@ bulkPricingPackageMonitoring:
|
||||
|
||||
# Configurations for integration with Brand Safety Alliance (BSA) API
|
||||
bsa:
|
||||
# Algorithm for calculating block list checksums
|
||||
bsaChecksumAlgorithm: SHA-256
|
||||
# The time allotted to every BSA cron job.
|
||||
bsaLockLeaseExpiryMinutes: 30
|
||||
# Desired time between successive downloads.
|
||||
bsaDownloadIntervalMinutes: 30
|
||||
# Max time period during which downloads can be skipped because checksums have
|
||||
# not changed from the previous one.
|
||||
bsaMaxNopIntervalHours: 24
|
||||
# A very lax upper bound of the time it takes to execute a transaction that
|
||||
# mutates a domain. Please See `BsaRefreshAction` for use case.
|
||||
domainTxnMaxDurationSeconds: 60
|
||||
# Number of entities (labels and unblockable domains) to process in a single
|
||||
# DB transaction.
|
||||
bsaTxnBatchSize: 1000
|
||||
|
||||
# Http endpoint for acquiring Auth tokens.
|
||||
authUrl: "https://"
|
||||
# Auth token expiry.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
package google.registry.dns;
|
||||
|
||||
import static google.registry.config.RegistryEnvironment.PRODUCTION;
|
||||
import static google.registry.util.RegistryEnvironment.PRODUCTION;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.monitoring.metrics.DistributionFitter;
|
||||
@@ -24,7 +24,7 @@ import com.google.monitoring.metrics.FibonacciFitter;
|
||||
import com.google.monitoring.metrics.IncrementableMetric;
|
||||
import com.google.monitoring.metrics.LabelDescriptor;
|
||||
import com.google.monitoring.metrics.MetricRegistryImpl;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
|
||||
@@ -13,12 +13,18 @@
|
||||
<load-on-startup>1</load-on-startup>
|
||||
</servlet>
|
||||
|
||||
<!-- Test action -->
|
||||
<!-- Download action -->
|
||||
<servlet-mapping>
|
||||
<servlet-name>bsa-servlet</servlet-name>
|
||||
<url-pattern>/_dr/task/bsaDownload</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- Refresh action -->
|
||||
<servlet-mapping>
|
||||
<servlet-name>bsa-servlet</servlet-name>
|
||||
<url-pattern>/_dr/task/bsaRefresh</url-pattern>
|
||||
</servlet-mapping>
|
||||
|
||||
<!-- Security config -->
|
||||
<security-constraint>
|
||||
<web-resource-collection>
|
||||
|
||||
@@ -162,4 +162,15 @@
|
||||
</description>
|
||||
<schedule>0 15 * * 1</schedule>
|
||||
</task>
|
||||
|
||||
<task>
|
||||
<url><![CDATA[/_dr/task/bsaDownload]]></url>
|
||||
<name>bsaDownload</name>
|
||||
<service>bsa</service>
|
||||
<description>
|
||||
Downloads the BSA block lists and processes the changes.
|
||||
</description>
|
||||
<!-- Runs every hour. -->
|
||||
<schedule>0 * * * *</schedule>
|
||||
</task>
|
||||
</entries>
|
||||
|
||||
@@ -26,7 +26,6 @@ import com.google.common.net.InetAddresses;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.flows.EppException.AuthenticationErrorException;
|
||||
import google.registry.flows.certs.CertificateChecker;
|
||||
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
|
||||
@@ -34,6 +33,7 @@ import google.registry.model.registrar.Registrar;
|
||||
import google.registry.request.Header;
|
||||
import google.registry.util.CidrAddressBlock;
|
||||
import google.registry.util.ProxyHttpHeaders;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.net.InetAddress;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -40,6 +40,7 @@ import static google.registry.flows.domain.DomainFlowUtils.verifyClaimsNoticeIfA
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyClaimsPeriodNotEnded;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyLaunchPhaseMatchesRegistryPhase;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyNoCodeMarks;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyNotBlockedByBsa;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyNotReserved;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyPremiumNameIsNotBlocked;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive;
|
||||
@@ -168,6 +169,7 @@ import org.joda.time.Duration;
|
||||
* @error {@link DomainFlowUtils.CurrencyUnitMismatchException}
|
||||
* @error {@link DomainFlowUtils.CurrencyValueScaleException}
|
||||
* @error {@link DomainFlowUtils.DashesInThirdAndFourthException}
|
||||
* @error {@link DomainFlowUtils.DomainLabelBlockedByBsaException}
|
||||
* @error {@link DomainFlowUtils.DomainLabelTooLongException}
|
||||
* @error {@link DomainFlowUtils.DomainReservedException}
|
||||
* @error {@link DomainFlowUtils.DuplicateContactForRoleException}
|
||||
@@ -328,6 +330,7 @@ public final class DomainCreateFlow implements MutatingFlow {
|
||||
.verifySignedMarks(launchCreate.get().getSignedMarks(), domainLabel, now)
|
||||
.getId();
|
||||
}
|
||||
verifyNotBlockedByBsa(domainLabel, tld, now);
|
||||
flowCustomLogic.afterValidation(
|
||||
DomainCreateFlowCustomLogic.AfterValidationParameters.newBuilder()
|
||||
.setDomainName(domainName)
|
||||
|
||||
@@ -25,11 +25,13 @@ import static com.google.common.collect.Iterables.any;
|
||||
import static com.google.common.collect.Sets.difference;
|
||||
import static com.google.common.collect.Sets.intersection;
|
||||
import static com.google.common.collect.Sets.union;
|
||||
import static google.registry.bsa.persistence.BsaLabelUtils.isLabelBlocked;
|
||||
import static google.registry.model.domain.Domain.MAX_REGISTRATION_YEARS;
|
||||
import static google.registry.model.tld.Tld.TldState.GENERAL_AVAILABILITY;
|
||||
import static google.registry.model.tld.Tld.TldState.PREDELEGATION;
|
||||
import static google.registry.model.tld.Tld.TldState.QUIET_PERIOD;
|
||||
import static google.registry.model.tld.Tld.TldState.START_DATE_SUNRISE;
|
||||
import static google.registry.model.tld.Tld.isEnrolledWithBsa;
|
||||
import static google.registry.model.tld.Tlds.findTldForName;
|
||||
import static google.registry.model.tld.Tlds.getTlds;
|
||||
import static google.registry.model.tld.label.ReservationType.ALLOWED_IN_SUNRISE;
|
||||
@@ -259,6 +261,23 @@ public class DomainFlowUtils {
|
||||
return idnTableName.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the {@code domainLabel} is not blocked by any BSA block label for the given
|
||||
* {@code tld} at the specified time.
|
||||
*
|
||||
* @throws DomainLabelBlockedByBsaException
|
||||
*/
|
||||
public static void verifyNotBlockedByBsa(String domainLabel, Tld tld, DateTime now)
|
||||
throws DomainLabelBlockedByBsaException {
|
||||
if (isBlockedByBsa(domainLabel, tld, now)) {
|
||||
throw new DomainLabelBlockedByBsaException();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isBlockedByBsa(String domainLabel, Tld tld, DateTime now) {
|
||||
return isEnrolledWithBsa(tld, now) && isLabelBlocked(domainLabel);
|
||||
}
|
||||
|
||||
/** Returns whether a given domain create request is for a valid anchor tenant. */
|
||||
public static boolean isAnchorTenant(
|
||||
InternetDomainName domainName,
|
||||
@@ -500,7 +519,7 @@ public class DomainFlowUtils {
|
||||
private static final ImmutableSet<ReservationType> RESERVED_TYPES =
|
||||
ImmutableSet.of(RESERVED_FOR_SPECIFIC_USE, RESERVED_FOR_ANCHOR_TENANT, FULLY_BLOCKED);
|
||||
|
||||
static boolean isReserved(InternetDomainName domainName, boolean isSunrise) {
|
||||
public static boolean isReserved(InternetDomainName domainName, boolean isSunrise) {
|
||||
ImmutableSet<ReservationType> types = getReservationTypes(domainName);
|
||||
return !Sets.intersection(types, RESERVED_TYPES).isEmpty()
|
||||
|| !(isSunrise || intersection(TYPES_ALLOWED_FOR_CREATE_ONLY_IN_SUNRISE, types).isEmpty());
|
||||
@@ -1742,4 +1761,12 @@ public class DomainFlowUtils {
|
||||
super("Registrar must be active in order to perform this operation");
|
||||
}
|
||||
}
|
||||
|
||||
/** Domain label is blocked by the Brand Safety Alliance. */
|
||||
static class DomainLabelBlockedByBsaException extends ParameterValuePolicyErrorException {
|
||||
public DomainLabelBlockedByBsaException() {
|
||||
// TODO(b/309174065): finalize the exception message.
|
||||
super("Domain label is blocked by the Brand Safety Alliance");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,12 +29,12 @@ import com.google.common.collect.Iterators;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.protobuf.Timestamp;
|
||||
import google.registry.batch.CloudTasksUtils;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Action.Service;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.security.XsrfTokenManager;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
|
||||
@@ -349,7 +349,7 @@ public class EntityYamlUtils {
|
||||
@Override
|
||||
public TimedTransitionProperty<Money> deserialize(JsonParser jp, DeserializationContext context)
|
||||
throws IOException {
|
||||
SortedMap<String, LinkedHashMap> valueMap = jp.readValueAs(SortedMap.class);
|
||||
SortedMap<String, LinkedHashMap<String, Object>> valueMap = jp.readValueAs(SortedMap.class);
|
||||
return TimedTransitionProperty.fromValueMap(
|
||||
valueMap.keySet().stream()
|
||||
.collect(
|
||||
@@ -359,7 +359,7 @@ public class EntityYamlUtils {
|
||||
key ->
|
||||
Money.of(
|
||||
CurrencyUnit.of(valueMap.get(key).get("currency").toString()),
|
||||
(double) valueMap.get(key).get("amount")))));
|
||||
new BigDecimal(String.valueOf(valueMap.get(key).get("amount")))))));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.ImmutableSortedMap;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Streams;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.pricing.StaticPremiumListPricingEngine;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.RegistrarAddress;
|
||||
@@ -40,6 +39,7 @@ import google.registry.model.tld.label.PremiumList;
|
||||
import google.registry.model.tld.label.PremiumListDao;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.CidrAddressBlock;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -22,6 +22,7 @@ import static com.google.common.base.Strings.nullToEmpty;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
|
||||
import static com.google.common.collect.Sets.immutableEnumSet;
|
||||
import static com.google.common.collect.Streams.stream;
|
||||
import static com.google.common.io.BaseEncoding.base64;
|
||||
import static google.registry.config.RegistryConfig.getDefaultRegistrarWhoisServer;
|
||||
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
|
||||
@@ -794,6 +795,24 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
}
|
||||
}
|
||||
|
||||
// Making sure there's no registrar with the same ianaId already in the system
|
||||
private static boolean isNotADuplicateIanaId(
|
||||
Iterable<Registrar> registrars, Registrar newInstance) {
|
||||
// Return early if newly build registrar is not type REAL or ianaId is
|
||||
// reserved by ICANN - https://www.iana.org/assignments/registrar-ids/registrar-ids.xhtml
|
||||
if (!Type.REAL.equals(newInstance.type)
|
||||
|| ImmutableSet.of(1L, 8L).contains(newInstance.ianaIdentifier)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return stream(registrars)
|
||||
.filter(registrar -> Type.REAL.equals(registrar.getType()))
|
||||
.filter(registrar -> !Objects.equals(newInstance.registrarId, registrar.getRegistrarId()))
|
||||
.noneMatch(
|
||||
registrar ->
|
||||
Objects.equals(newInstance.ianaIdentifier, registrar.getIanaIdentifier()));
|
||||
}
|
||||
|
||||
public Builder setContactsRequireSyncing(boolean contactsRequireSyncing) {
|
||||
getInstance().contactsRequireSyncing = contactsRequireSyncing;
|
||||
return this;
|
||||
@@ -912,6 +931,15 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
|
||||
"Supplied IANA ID is not valid for %s registrar type: %s",
|
||||
getInstance().type, getInstance().ianaIdentifier));
|
||||
|
||||
// We do not allow creating Real registrars with IANA ID that's already in the system
|
||||
// b/315007360 - for more details
|
||||
checkArgument(
|
||||
isNotADuplicateIanaId(loadAllCached(), getInstance()),
|
||||
String.format(
|
||||
"Rejected attempt to create a registrar with ianaId that's already in the system -"
|
||||
+ " %s",
|
||||
getInstance().ianaIdentifier));
|
||||
|
||||
// In order to grant access to real TLDs, the registrar must have a corresponding billing
|
||||
// account ID for that TLD's billing currency.
|
||||
ImmutableSet<String> nonBillableTlds =
|
||||
|
||||
@@ -256,6 +256,11 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
return VKey.create(Tld.class, tldStr);
|
||||
}
|
||||
|
||||
/** Checks if {@code tld} is enrolled with BSA. */
|
||||
public static boolean isEnrolledWithBsa(Tld tld, DateTime now) {
|
||||
return tld.getBsaEnrollStartTime().orElse(END_OF_TIME).isBefore(now);
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the pricing engine that this TLD uses.
|
||||
*
|
||||
@@ -550,9 +555,15 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
@JsonSerialize(using = SortedEnumSetSerializer.class)
|
||||
Set<IdnTableEnum> idnTables;
|
||||
|
||||
// TODO(11/30/2023): uncomment below two lines
|
||||
// /** The start time of this TLD's enrollment in the BSA program, if applicable. */
|
||||
// @JsonIgnore @Nullable DateTime bsaEnrollStartTime;
|
||||
/**
|
||||
* The start time of this TLD's enrollment in the BSA program, if applicable.
|
||||
*
|
||||
* <p>This property is excluded from source-based configuration and is managed directly in the
|
||||
* database.
|
||||
*/
|
||||
// TODO(b/309175410): implement setup and cleanup procedure for joining or leaving BSA, and see
|
||||
// if it can be integrated with the ConfigTldCommand.
|
||||
@JsonIgnore @Nullable DateTime bsaEnrollStartTime;
|
||||
|
||||
public String getTldStr() {
|
||||
return tldStr;
|
||||
@@ -574,12 +585,9 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
}
|
||||
|
||||
/** Returns the time when this TLD was enrolled in the Brand Safety Alliance (BSA) program. */
|
||||
@JsonIgnore // Annotation can be removed once we add the field and annotate it.
|
||||
@Nullable
|
||||
public DateTime getBsaEnrollStartTime() {
|
||||
// TODO(11/30/2023): uncomment below.
|
||||
// return this.bsaEnrollStartTime;
|
||||
return null;
|
||||
@JsonIgnore
|
||||
public Optional<DateTime> getBsaEnrollStartTime() {
|
||||
return Optional.ofNullable(this.bsaEnrollStartTime);
|
||||
}
|
||||
|
||||
/** Retrieve whether invoicing is enabled. */
|
||||
@@ -1101,10 +1109,9 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setBsaEnrollStartTime(DateTime enrollTime) {
|
||||
public Builder setBsaEnrollStartTime(Optional<DateTime> enrollTime) {
|
||||
// TODO(b/309175133): forbid if enrolled with BSA
|
||||
// TODO(11/30/2023): uncomment below line
|
||||
// getInstance().bsaEnrollStartTime = enrollTime;
|
||||
getInstance().bsaEnrollStartTime = enrollTime.orElse(null);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import static com.google.common.base.Strings.emptyToNull;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.Maps.filterValues;
|
||||
import static google.registry.model.CacheUtils.memoizeWithShortExpiration;
|
||||
import static google.registry.model.tld.Tld.isEnrolledWithBsa;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.util.CollectionUtils.entriesToImmutableMap;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
@@ -38,6 +39,7 @@ import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import javax.persistence.EntityManager;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Utilities for finding and listing {@link Tld} entities. */
|
||||
public final class Tlds {
|
||||
@@ -151,4 +153,9 @@ public final class Tlds {
|
||||
findTldForName(domainName).orElse(null),
|
||||
"Domain name is not under a recognized TLD: %s", domainName.toString());
|
||||
}
|
||||
|
||||
/** Returns true if at least one TLD is enrolled {@code now}. */
|
||||
public static boolean hasActiveBsaEnrollment(DateTime now) {
|
||||
return getTldEntitiesOfType(TldType.REAL).stream().anyMatch(tld -> isEnrolledWithBsa(tld, now));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,9 +19,14 @@ import dagger.Component;
|
||||
import dagger.Lazy;
|
||||
import google.registry.config.CredentialModule;
|
||||
import google.registry.config.RegistryConfig.ConfigModule;
|
||||
import google.registry.keyring.KeyringModule;
|
||||
import google.registry.keyring.secretmanager.SecretManagerKeyringModule;
|
||||
import google.registry.module.bsa.BsaRequestComponent.BsaRequestComponentModule;
|
||||
import google.registry.monitoring.whitebox.StackdriverModule;
|
||||
import google.registry.persistence.PersistenceModule;
|
||||
import google.registry.privileges.secretmanager.SecretManagerModule;
|
||||
import google.registry.request.Modules.GsonModule;
|
||||
import google.registry.request.Modules.UrlConnectionServiceModule;
|
||||
import google.registry.request.Modules.UserServiceModule;
|
||||
import google.registry.request.auth.AuthModule;
|
||||
import google.registry.util.UtilsModule;
|
||||
@@ -31,13 +36,18 @@ import javax.inject.Singleton;
|
||||
@Component(
|
||||
modules = {
|
||||
AuthModule.class,
|
||||
UtilsModule.class,
|
||||
UserServiceModule.class,
|
||||
GsonModule.class,
|
||||
BsaRequestComponentModule.class,
|
||||
ConfigModule.class,
|
||||
StackdriverModule.class,
|
||||
CredentialModule.class,
|
||||
BsaRequestComponentModule.class
|
||||
GsonModule.class,
|
||||
PersistenceModule.class,
|
||||
KeyringModule.class,
|
||||
SecretManagerKeyringModule.class,
|
||||
SecretManagerModule.class,
|
||||
StackdriverModule.class,
|
||||
UrlConnectionServiceModule.class,
|
||||
UserServiceModule.class,
|
||||
UtilsModule.class
|
||||
})
|
||||
interface BsaComponent {
|
||||
BsaRequestHandler requestHandler();
|
||||
|
||||
@@ -16,7 +16,8 @@ package google.registry.module.bsa;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Subcomponent;
|
||||
import google.registry.bsa.PlaceholderAction;
|
||||
import google.registry.bsa.BsaDownloadAction;
|
||||
import google.registry.bsa.BsaRefreshAction;
|
||||
import google.registry.request.RequestComponentBuilder;
|
||||
import google.registry.request.RequestModule;
|
||||
import google.registry.request.RequestScope;
|
||||
@@ -28,7 +29,9 @@ import google.registry.request.RequestScope;
|
||||
})
|
||||
interface BsaRequestComponent {
|
||||
|
||||
PlaceholderAction bsaAction();
|
||||
BsaDownloadAction bsaDownloadAction();
|
||||
|
||||
BsaRefreshAction bsaRefreshAction();
|
||||
|
||||
@Subcomponent.Builder
|
||||
abstract class Builder implements RequestComponentBuilder<BsaRequestComponent> {
|
||||
|
||||
@@ -38,6 +38,7 @@ import google.registry.persistence.JpaRetries;
|
||||
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
|
||||
import google.registry.persistence.VKey;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import google.registry.util.Retrier;
|
||||
import google.registry.util.SystemSleeper;
|
||||
import java.io.Serializable;
|
||||
@@ -164,7 +165,10 @@ public class JpaTransactionManagerImpl implements JpaTransactionManager {
|
||||
if (!getHibernateAllowNestedTransactions()) {
|
||||
throw new IllegalStateException(NESTED_TRANSACTION_MESSAGE);
|
||||
}
|
||||
logger.atWarning().withStackTrace(StackSize.MEDIUM).log(NESTED_TRANSACTION_MESSAGE);
|
||||
if (RegistryEnvironment.get() != RegistryEnvironment.PRODUCTION
|
||||
&& RegistryEnvironment.get() != RegistryEnvironment.UNITTEST) {
|
||||
logger.atWarning().withStackTrace(StackSize.MEDIUM).log(NESTED_TRANSACTION_MESSAGE);
|
||||
}
|
||||
// This prevents inner transaction from retrying, thus avoiding a cascade retry effect.
|
||||
return transactNoRetry(work, isolationLevel);
|
||||
}
|
||||
|
||||
@@ -20,10 +20,10 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
import com.google.appengine.api.utils.SystemProperty;
|
||||
import com.google.appengine.api.utils.SystemProperty.Environment.Value;
|
||||
import com.google.common.base.Suppliers;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.persistence.DaggerPersistenceComponent;
|
||||
import google.registry.tools.RegistryToolEnvironment;
|
||||
import google.registry.util.NonFinalForTesting;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** Factory class to create {@link TransactionManager} instance. */
|
||||
|
||||
@@ -17,6 +17,8 @@ package google.registry.rdap;
|
||||
import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_OK;
|
||||
import static com.google.common.net.HttpHeaders.ACCEPT_ENCODING;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.request.UrlConnectionUtils.gUnzipBytes;
|
||||
import static google.registry.request.UrlConnectionUtils.isGZipped;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
@@ -115,7 +117,13 @@ public final class UpdateRegistrarRdapBaseUrlsAction implements Runnable {
|
||||
if (connection.getResponseCode() != STATUS_CODE_OK) {
|
||||
throw new UrlConnectionException("Failed to load RDAP base URLs from ICANN", connection);
|
||||
}
|
||||
csvString = new String(UrlConnectionUtils.getResponseBytes(connection), UTF_8);
|
||||
// With GZIP encoding header in the request (see above) ICANN had still sent response in plain
|
||||
// text until at some point they started sending the response encoded in gzip, which broke our
|
||||
// parsing of the response. Because of that it was decided to check for the response encoding,
|
||||
// just in case they ever start sending a plain text again.
|
||||
byte[] responseBytes = UrlConnectionUtils.getResponseBytes(connection);
|
||||
csvString =
|
||||
new String(isGZipped(responseBytes) ? gUnzipBytes(responseBytes) : responseBytes, UTF_8);
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
|
||||
@@ -40,7 +40,6 @@ import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.io.BaseEncoding;
|
||||
import google.registry.beam.rde.RdePipeline;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.gcs.GcsUtils;
|
||||
import google.registry.keyring.api.KeyModule.Key;
|
||||
import google.registry.model.common.Cursor;
|
||||
@@ -57,6 +56,7 @@ import google.registry.request.RequestParameters;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import google.registry.xml.ValidationMode;
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -29,7 +29,6 @@ import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.batch.CloudTasksUtils;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.persistence.PersistenceModule;
|
||||
import google.registry.reporting.ReportingModule;
|
||||
import google.registry.request.Action;
|
||||
@@ -38,6 +37,7 @@ import google.registry.request.Parameter;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.io.IOException;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
package google.registry.reporting.icann;
|
||||
|
||||
import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_BAD_REQUEST;
|
||||
import static com.google.api.client.http.HttpStatusCodes.STATUS_CODE_OK;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.net.MediaType.CSV_UTF_8;
|
||||
@@ -38,6 +37,7 @@ import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
@@ -90,30 +90,31 @@ public class IcannHttpReporter {
|
||||
UrlConnectionUtils.setPayload(connection, reportBytes, CSV_UTF_8.toString());
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
|
||||
int responseCode;
|
||||
byte[] content;
|
||||
int responseCode = 0;
|
||||
byte[] content = null;
|
||||
try {
|
||||
responseCode = connection.getResponseCode();
|
||||
// Only responses with a 200 or 400 status have a body. For everything else, we can return
|
||||
// false early.
|
||||
if (responseCode != STATUS_CODE_OK && responseCode != STATUS_CODE_BAD_REQUEST) {
|
||||
logger.atWarning().log("Connection to ICANN server failed", connection);
|
||||
content = UrlConnectionUtils.getResponseBytes(connection);
|
||||
if (responseCode != STATUS_CODE_OK) {
|
||||
XjcIirdeaResult result = parseResult(content);
|
||||
logger.atWarning().log(
|
||||
"PUT rejected, status code %s:\n%s\n%s",
|
||||
result.getCode().getValue(), result.getMsg(), result.getDescription());
|
||||
return false;
|
||||
}
|
||||
content = UrlConnectionUtils.getResponseBytes(connection);
|
||||
} catch (IOException e) {
|
||||
logger.atWarning().withCause(e).log(
|
||||
"Connection to ICANN server failed with responseCode %s and connection %s",
|
||||
responseCode == 0 ? "not available" : responseCode, connection);
|
||||
return false;
|
||||
} catch (XmlException e) {
|
||||
logger.atWarning().withCause(e).log(
|
||||
"Failed to parse ICANN response with responseCode %s and content %s",
|
||||
responseCode, new String(content, StandardCharsets.UTF_8));
|
||||
return false;
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
// We know that an HTTP 200 response can only contain a result code of
|
||||
// 1000 (i. e. success), there is no need to parse it.
|
||||
// See: https://tools.ietf.org/html/draft-lozano-icann-registry-interfaces-13#page-16
|
||||
if (responseCode != STATUS_CODE_OK) {
|
||||
XjcIirdeaResult result = parseResult(content);
|
||||
logger.atWarning().log(
|
||||
"PUT rejected, status code %s:\n%s\n%s",
|
||||
result.getCode().getValue(), result.getMsg(), result.getDescription());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -164,4 +165,5 @@ public class IcannHttpReporter {
|
||||
reportType));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ import com.google.common.flogger.FluentLogger;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.batch.CloudTasksUtils;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.keyring.api.KeyModule.Key;
|
||||
import google.registry.reporting.ReportingModule;
|
||||
import google.registry.request.Action;
|
||||
@@ -38,6 +37,7 @@ import google.registry.request.Parameter;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.io.IOException;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
@@ -25,37 +25,66 @@ import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.net.MediaType;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Random;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import org.apache.commons.compress.utils.IOUtils;
|
||||
|
||||
/** Utilities for common functionality relating to {@link URLConnection}s. */
|
||||
public final class UrlConnectionUtils {
|
||||
|
||||
private UrlConnectionUtils() {}
|
||||
|
||||
/** Retrieves the response from the given connection as a byte array. */
|
||||
public static byte[] getResponseBytes(URLConnection connection) throws IOException {
|
||||
try (InputStream is = connection.getInputStream()) {
|
||||
/**
|
||||
* Retrieves the response from the given connection as a byte array.
|
||||
*
|
||||
* <p>Note that in the event the response code is 4XX or 5XX, we use the error stream as any
|
||||
* payload is included there.
|
||||
*
|
||||
* @see HttpURLConnection#getErrorStream()
|
||||
*/
|
||||
public static byte[] getResponseBytes(HttpURLConnection connection) throws IOException {
|
||||
int responseCode = connection.getResponseCode();
|
||||
try (InputStream is =
|
||||
responseCode < 400 ? connection.getInputStream() : connection.getErrorStream()) {
|
||||
return ByteStreams.toByteArray(is);
|
||||
} catch (NullPointerException e) {
|
||||
return new byte[] {};
|
||||
}
|
||||
}
|
||||
|
||||
/** Decodes compressed data in GZIP format. */
|
||||
public static byte[] gUnzipBytes(byte[] bytes) throws IOException {
|
||||
try (GZIPInputStream inputStream = new GZIPInputStream(new ByteArrayInputStream(bytes))) {
|
||||
return IOUtils.toByteArray(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks whether {@code bytes} are GZIP encoded. */
|
||||
public static boolean isGZipped(byte[] bytes) {
|
||||
// See GzipOutputStream.writeHeader()
|
||||
return (bytes.length > 2 && bytes[0] == (byte) (GZIPInputStream.GZIP_MAGIC))
|
||||
&& (bytes[1] == (byte) (GZIPInputStream.GZIP_MAGIC >> 8));
|
||||
}
|
||||
|
||||
/** Sets auth on the given connection with the given username/password. */
|
||||
public static void setBasicAuth(URLConnection connection, String username, String password) {
|
||||
public static void setBasicAuth(HttpURLConnection connection, String username, String password) {
|
||||
setBasicAuth(connection, String.format("%s:%s", username, password));
|
||||
}
|
||||
|
||||
/** Sets auth on the given connection with the given string, formatted "username:password". */
|
||||
public static void setBasicAuth(URLConnection connection, String usernameAndPassword) {
|
||||
public static void setBasicAuth(HttpURLConnection connection, String usernameAndPassword) {
|
||||
String token = base64().encode(usernameAndPassword.getBytes(UTF_8));
|
||||
connection.setRequestProperty(AUTHORIZATION, "Basic " + token);
|
||||
}
|
||||
|
||||
/** Sets the given byte[] payload on the given connection with a particular content type. */
|
||||
public static void setPayload(URLConnection connection, byte[] bytes, String contentType)
|
||||
public static void setPayload(HttpURLConnection connection, byte[] bytes, String contentType)
|
||||
throws IOException {
|
||||
connection.setRequestProperty(CONTENT_TYPE, contentType);
|
||||
connection.setDoOutput(true);
|
||||
@@ -72,7 +101,7 @@ public final class UrlConnectionUtils {
|
||||
* @see <a href="http://www.ietf.org/rfc/rfc2388.txt">RFC2388 - Returning Values from Forms</a>
|
||||
*/
|
||||
public static void setPayloadMultipart(
|
||||
URLConnection connection,
|
||||
HttpURLConnection connection,
|
||||
String name,
|
||||
String filename,
|
||||
MediaType contentType,
|
||||
|
||||
@@ -20,13 +20,13 @@ import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.console.User;
|
||||
import google.registry.model.console.UserDao;
|
||||
import google.registry.request.auth.AuthModule.IapOidc;
|
||||
import google.registry.request.auth.AuthModule.RegularOidc;
|
||||
import google.registry.request.auth.AuthModule.RegularOidcFallback;
|
||||
import google.registry.request.auth.AuthSettings.AuthLevel;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -53,6 +53,7 @@ import java.net.URL;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import javax.inject.Inject;
|
||||
import org.joda.time.Duration;
|
||||
@@ -126,55 +127,62 @@ public final class NordnUploadAction implements Runnable {
|
||||
phase.equals(PARAM_LORDN_PHASE_SUNRISE) || phase.equals(PARAM_LORDN_PHASE_CLAIMS),
|
||||
"Invalid phase specified to NordnUploadAction: %s.",
|
||||
phase);
|
||||
tm().transact(
|
||||
() -> {
|
||||
// Note here that we load all domains pending Nordn in one batch, which should not
|
||||
// be a problem for the rate of domain registration that we see. If we anticipate
|
||||
// a peak in claims during TLD launch (sunrise is NOT first-come-first-serve, so
|
||||
// there should be no expectation of a peak during it), we can consider temporarily
|
||||
// increasing the frequency of Nordn upload to reduce the size of each batch.
|
||||
//
|
||||
// We did not further divide the domains into smaller batches because the
|
||||
// read-upload-write operation per small batch needs to be inside a single
|
||||
// transaction to prevent race conditions, and running several uploads in rapid
|
||||
// sucession will likely overwhelm the MarksDB upload server, which recommands a
|
||||
// maximum upload frequency of every 3 hours.
|
||||
//
|
||||
// See:
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-regext-tmch-func-spec-01#section-5.2.3.3
|
||||
List<Domain> domains =
|
||||
tm().createQueryComposer(Domain.class)
|
||||
.where("lordnPhase", EQ, LordnPhase.valueOf(Ascii.toUpperCase(phase)))
|
||||
.where("tld", EQ, tld)
|
||||
.orderBy("creationTime")
|
||||
.list();
|
||||
if (domains.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
StringBuilder csv = new StringBuilder();
|
||||
ImmutableList.Builder<Domain> newDomains = new ImmutableList.Builder<>();
|
||||
Optional<URL> uploadUrl =
|
||||
tm().transact(
|
||||
() -> {
|
||||
// Note here that we load all domains pending Nordn in one batch, which should not
|
||||
// be a problem for the rate of domain registration that we see. If we anticipate
|
||||
// a peak in claims during TLD launch (sunrise is NOT first-come-first-serve, so
|
||||
// there should be no expectation of a peak during it), we can consider
|
||||
// temporarily increasing the frequency of Nordn upload to reduce the size of each
|
||||
// batch.
|
||||
//
|
||||
// We did not further divide the domains into smaller batches because the
|
||||
// read-upload-write operation per small batch needs to be inside a single
|
||||
// transaction to prevent race conditions, and running several uploads in rapid
|
||||
// succession will likely overwhelm the MarksDB upload server, which recommends a
|
||||
// maximum upload frequency of every 3 hours.
|
||||
//
|
||||
// See:
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-regext-tmch-func-spec-01#section-5.2.3.3
|
||||
List<Domain> domains =
|
||||
tm().createQueryComposer(Domain.class)
|
||||
.where("lordnPhase", EQ, LordnPhase.valueOf(Ascii.toUpperCase(phase)))
|
||||
.where("tld", EQ, tld)
|
||||
.orderBy("creationTime")
|
||||
.list();
|
||||
if (domains.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
StringBuilder csv = new StringBuilder();
|
||||
ImmutableList.Builder<Domain> newDomains = new ImmutableList.Builder<>();
|
||||
|
||||
domains.forEach(
|
||||
domain -> {
|
||||
if (phase.equals(PARAM_LORDN_PHASE_SUNRISE)) {
|
||||
csv.append(getCsvLineForSunriseDomain(domain)).append('\n');
|
||||
} else {
|
||||
csv.append(getCsvLineForClaimsDomain(domain)).append('\n');
|
||||
}
|
||||
Domain newDomain = domain.asBuilder().setLordnPhase(LordnPhase.NONE).build();
|
||||
newDomains.add(newDomain);
|
||||
});
|
||||
String columns =
|
||||
phase.equals(PARAM_LORDN_PHASE_SUNRISE) ? COLUMNS_SUNRISE : COLUMNS_CLAIMS;
|
||||
String header =
|
||||
String.format("1,%s,%d\n%s\n", clock.nowUtc(), domains.size(), columns);
|
||||
try {
|
||||
uploadCsvToLordn(String.format("/LORDN/%s/%s", tld, phase), header + csv);
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
tm().updateAll(newDomains.build());
|
||||
});
|
||||
domains.forEach(
|
||||
domain -> {
|
||||
if (phase.equals(PARAM_LORDN_PHASE_SUNRISE)) {
|
||||
csv.append(getCsvLineForSunriseDomain(domain)).append('\n');
|
||||
} else {
|
||||
csv.append(getCsvLineForClaimsDomain(domain)).append('\n');
|
||||
}
|
||||
Domain newDomain =
|
||||
domain.asBuilder().setLordnPhase(LordnPhase.NONE).build();
|
||||
newDomains.add(newDomain);
|
||||
});
|
||||
String columns =
|
||||
phase.equals(PARAM_LORDN_PHASE_SUNRISE) ? COLUMNS_SUNRISE : COLUMNS_CLAIMS;
|
||||
String header =
|
||||
String.format("1,%s,%d\n%s\n", clock.nowUtc(), domains.size(), columns);
|
||||
try {
|
||||
URL url =
|
||||
uploadCsvToLordn(String.format("/LORDN/%s/%s", tld, phase), header + csv);
|
||||
tm().updateAll(newDomains.build());
|
||||
return Optional.of(url);
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
uploadUrl.ifPresent(
|
||||
url -> cloudTasksUtils.enqueue(NordnVerifyAction.QUEUE, makeVerifyTask(url)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,7 +194,7 @@ public final class NordnUploadAction implements Runnable {
|
||||
* @see <a href="http://tools.ietf.org/html/draft-lozano-tmch-func-spec-08#section-6.3">TMCH
|
||||
* functional specifications - LORDN File</a>
|
||||
*/
|
||||
private void uploadCsvToLordn(String urlPath, String csvData)
|
||||
private URL uploadCsvToLordn(String urlPath, String csvData)
|
||||
throws IOException, GeneralSecurityException {
|
||||
String url = tmchMarksdbUrl + urlPath;
|
||||
logger.atInfo().log(
|
||||
@@ -222,7 +230,7 @@ public final class NordnUploadAction implements Runnable {
|
||||
actionLogId),
|
||||
connection);
|
||||
}
|
||||
cloudTasksUtils.enqueue(NordnVerifyAction.QUEUE, makeVerifyTask(new URL(location)));
|
||||
return new URL(location);
|
||||
} catch (IOException e) {
|
||||
throw new IOException(String.format("Error connecting to MarksDB at URL %s", url), e);
|
||||
} finally {
|
||||
|
||||
@@ -72,9 +72,9 @@ public class ConfigureTldCommand extends MutatingCommand {
|
||||
boolean breakglass;
|
||||
|
||||
@Parameter(
|
||||
names = {"-d", "--dryrun"},
|
||||
names = {"-d", "--dry_run"},
|
||||
description = "Does not execute the entity mutation")
|
||||
boolean dryrun;
|
||||
boolean dryRun;
|
||||
|
||||
@Inject ObjectMapper mapper;
|
||||
|
||||
@@ -122,6 +122,13 @@ public class ConfigureTldCommand extends MutatingCommand {
|
||||
checkPremiumList(newTld);
|
||||
checkDnsWriters(newTld);
|
||||
checkCurrency(newTld);
|
||||
// bsaEnrollStartTime only exists in DB. Need to carry it over to the updated copy. See Tld.java
|
||||
// for more information.
|
||||
Optional<DateTime> bsaEnrollTime =
|
||||
Optional.ofNullable(oldTld).flatMap(Tld::getBsaEnrollStartTime);
|
||||
if (bsaEnrollTime.isPresent()) {
|
||||
newTld = newTld.asBuilder().setBsaEnrollStartTime(bsaEnrollTime).build();
|
||||
}
|
||||
// Set the new TLD to breakglass mode if breakglass flag was used
|
||||
if (breakglass) {
|
||||
newTld = newTld.asBuilder().setBreakglassMode(true).build();
|
||||
@@ -131,7 +138,7 @@ public class ConfigureTldCommand extends MutatingCommand {
|
||||
|
||||
@Override
|
||||
protected boolean dontRunCommand() {
|
||||
if (dryrun) {
|
||||
if (dryRun) {
|
||||
return true;
|
||||
}
|
||||
if (!newDiff) {
|
||||
|
||||
@@ -30,8 +30,8 @@ import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Streams;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -21,8 +21,8 @@ import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.config.SystemPropertySetter;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import google.registry.util.SystemPropertySetter;
|
||||
|
||||
/** Enum of production environments, used for the {@code --environment} flag. */
|
||||
public enum RegistryToolEnvironment {
|
||||
|
||||
@@ -22,10 +22,10 @@ import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.io.MoreFiles;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.OteAccountBuilder;
|
||||
import google.registry.tools.params.PathParameter;
|
||||
import google.registry.util.Clock;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import google.registry.util.StringGenerator;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
|
||||
@@ -67,9 +67,12 @@ abstract class UpdateOrDeleteAllocationTokensCommand extends ConfirmingCommand {
|
||||
checkArgument(!prefix.isEmpty(), "Provided prefix should not be blank");
|
||||
return tm().transact(
|
||||
() ->
|
||||
tm().loadAllOf(AllocationToken.class).stream()
|
||||
.filter(token -> token.getToken().startsWith(prefix))
|
||||
.map(AllocationToken::createVKey)
|
||||
tm().query(
|
||||
"SELECT token FROM AllocationToken WHERE token LIKE :prefix",
|
||||
String.class)
|
||||
.setParameter("prefix", String.format("%s%%", prefix))
|
||||
.getResultStream()
|
||||
.map(token -> VKey.create(AllocationToken.class, token))
|
||||
.collect(toImmutableList()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static google.registry.util.ListNamingUtils.convertFilePathToName;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import com.google.common.base.Strings;
|
||||
import google.registry.model.tld.label.PremiumList;
|
||||
@@ -29,6 +30,14 @@ import java.nio.file.Files;
|
||||
@Parameters(separators = " =", commandDescription = "Update a PremiumList in Database.")
|
||||
class UpdatePremiumListCommand extends CreateOrUpdatePremiumListCommand {
|
||||
|
||||
@Parameter(
|
||||
names = {"-d", "--dry_run"},
|
||||
description = "Does not execute the entity mutation")
|
||||
boolean dryRun;
|
||||
|
||||
// indicates if there is a new change made by this command
|
||||
private boolean newChange = false;
|
||||
|
||||
@Override
|
||||
protected String prompt() throws Exception {
|
||||
name = Strings.isNullOrEmpty(name) ? convertFilePathToName(inputFile) : name;
|
||||
@@ -43,8 +52,23 @@ class UpdatePremiumListCommand extends CreateOrUpdatePremiumListCommand {
|
||||
checkArgument(!inputData.isEmpty(), "New premium list data cannot be empty");
|
||||
currency = existingList.getCurrency();
|
||||
PremiumList updatedPremiumList = PremiumListUtils.parseToPremiumList(name, currency, inputData);
|
||||
return String.format(
|
||||
"Update premium list for %s?\n Old List: %s\n New List: %s",
|
||||
name, existingList, updatedPremiumList);
|
||||
if (!existingList
|
||||
.getLabelsToPrices()
|
||||
.entrySet()
|
||||
.equals(updatedPremiumList.getLabelsToPrices().entrySet())) {
|
||||
newChange = true;
|
||||
return String.format(
|
||||
"Update premium list for %s?\n Old List: %s\n New List: %s",
|
||||
name, existingList, updatedPremiumList);
|
||||
} else {
|
||||
return String.format(
|
||||
"This update contains no changes to the premium list for %s.\n List Contents: %s",
|
||||
name, existingList);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean dontRunCommand() {
|
||||
return dryRun || !newChange;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
|
||||
|
||||
import com.beust.jcommander.Parameters;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Command to update a Registrar. */
|
||||
|
||||
@@ -25,10 +25,10 @@ import com.beust.jcommander.Parameter;
|
||||
import com.beust.jcommander.Parameters;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Maps;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.model.tld.Tld.TldState;
|
||||
import google.registry.tools.params.StringListParameter;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -25,9 +25,9 @@ import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Streams;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.tools.server.VerifyOteAction;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
package google.registry.ui.server.registrar;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
import static google.registry.config.RegistryEnvironment.PRODUCTION;
|
||||
import static google.registry.ui.server.SoyTemplateUtils.CSS_RENAMING_MAP_SUPPLIER;
|
||||
import static google.registry.util.RegistryEnvironment.PRODUCTION;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
|
||||
|
||||
import com.google.common.base.Ascii;
|
||||
@@ -24,7 +24,6 @@ import com.google.common.base.Supplier;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.template.soy.tofu.SoyTofu;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.OteAccountBuilder;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Action.Method;
|
||||
@@ -34,6 +33,7 @@ import google.registry.request.auth.AuthenticatedRegistrarAccessor;
|
||||
import google.registry.ui.server.SendEmailUtils;
|
||||
import google.registry.ui.server.SoyTemplateUtils;
|
||||
import google.registry.ui.soy.registrar.OteSetupConsoleSoyInfo;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import google.registry.util.StringGenerator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -26,7 +26,6 @@ import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import com.google.template.soy.tofu.SoyTofu;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.model.registrar.Registrar;
|
||||
import google.registry.model.registrar.Registrar.State;
|
||||
import google.registry.model.registrar.RegistrarAddress;
|
||||
@@ -44,6 +43,7 @@ import google.registry.ui.soy.registrar.ConsoleSoyInfo;
|
||||
import google.registry.ui.soy.registrar.ConsoleUtilsSoyInfo;
|
||||
import google.registry.ui.soy.registrar.FormsSoyInfo;
|
||||
import google.registry.ui.soy.registrar.RegistrarCreateConsoleSoyInfo;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import google.registry.util.StringGenerator;
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -27,7 +27,6 @@ import com.google.common.flogger.FluentLogger;
|
||||
import com.google.template.soy.data.SoyMapData;
|
||||
import com.google.template.soy.tofu.SoyTofu;
|
||||
import google.registry.config.RegistryConfig.Config;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.auth.Auth;
|
||||
@@ -36,6 +35,7 @@ import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAcce
|
||||
import google.registry.request.auth.AuthenticatedRegistrarAccessor.Role;
|
||||
import google.registry.ui.server.SoyTemplateUtils;
|
||||
import google.registry.ui.soy.registrar.ConsoleSoyInfo;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -18,11 +18,11 @@ import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.ImmutableSet.toImmutableSet;
|
||||
import static com.google.common.collect.Sets.difference;
|
||||
import static google.registry.config.RegistryEnvironment.PRODUCTION;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.security.JsonResponseHelper.Status.ERROR;
|
||||
import static google.registry.security.JsonResponseHelper.Status.SUCCESS;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
|
||||
import static google.registry.util.RegistryEnvironment.PRODUCTION;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.base.Ascii;
|
||||
@@ -37,7 +37,6 @@ import com.google.common.collect.Sets;
|
||||
import com.google.common.collect.Streams;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.batch.CloudTasksUtils;
|
||||
import google.registry.config.RegistryEnvironment;
|
||||
import google.registry.export.sheet.SyncRegistrarsSheetAction;
|
||||
import google.registry.flows.certs.CertificateChecker;
|
||||
import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
|
||||
@@ -61,6 +60,7 @@ import google.registry.ui.server.RegistrarFormFields;
|
||||
import google.registry.ui.server.SendEmailUtils;
|
||||
import google.registry.util.CollectionUtils;
|
||||
import google.registry.util.DiffUtils;
|
||||
import google.registry.util.RegistryEnvironment;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -14,34 +14,84 @@
|
||||
|
||||
package google.registry.whois;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static google.registry.flows.domain.DomainFlowUtils.isBlockedByBsa;
|
||||
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
||||
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
|
||||
import static google.registry.model.tld.Tlds.findTldForName;
|
||||
import static google.registry.model.tld.Tlds.getTlds;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
|
||||
|
||||
import com.google.auto.value.AutoValue;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Verify;
|
||||
import com.google.common.net.InternetDomainName;
|
||||
import google.registry.model.domain.Domain;
|
||||
import google.registry.model.tld.Tld;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Represents a WHOIS lookup on a domain name (i.e. SLD). */
|
||||
public class DomainLookupCommand extends DomainOrHostLookupCommand {
|
||||
public class DomainLookupCommand implements WhoisCommand {
|
||||
|
||||
private static final String ERROR_PREFIX = "Domain";
|
||||
|
||||
@VisibleForTesting final InternetDomainName domainName;
|
||||
|
||||
private final boolean fullOutput;
|
||||
private final boolean cached;
|
||||
private final String whoisRedactedEmailText;
|
||||
private final String domainBlockedByBsaTemplate;
|
||||
|
||||
public DomainLookupCommand(
|
||||
InternetDomainName domainName,
|
||||
boolean fullOutput,
|
||||
boolean cached,
|
||||
String whoisRedactedEmailText) {
|
||||
super(domainName, "Domain");
|
||||
String whoisRedactedEmailText,
|
||||
String domainBlockedByBsaTemplate) {
|
||||
this.domainName = checkNotNull(domainName, "domainOrHostName");
|
||||
this.fullOutput = fullOutput;
|
||||
this.cached = cached;
|
||||
this.whoisRedactedEmailText = whoisRedactedEmailText;
|
||||
this.domainBlockedByBsaTemplate = domainBlockedByBsaTemplate;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<WhoisResponse> getResponse(InternetDomainName domainName, DateTime now) {
|
||||
public final WhoisResponse executeQuery(final DateTime now) throws WhoisException {
|
||||
Optional<InternetDomainName> tld = findTldForName(domainName);
|
||||
// Google Registry Policy: Do not return records under TLDs for which we're not
|
||||
// authoritative.
|
||||
if (!tld.isPresent() || !getTlds().contains(tld.get().toString())) {
|
||||
throw new WhoisException(now, SC_NOT_FOUND, ERROR_PREFIX + " not found.");
|
||||
}
|
||||
// Include `getResponse` and `isBlockedByBsa` in one transaction to reduce latency.
|
||||
// Must pass the exceptions outside to throw.
|
||||
ResponseOrException result =
|
||||
tm().transact(
|
||||
() -> {
|
||||
final Optional<WhoisResponse> response = getResponse(domainName, now);
|
||||
if (response.isPresent()) {
|
||||
return ResponseOrException.of(response.get());
|
||||
}
|
||||
|
||||
String label = domainName.parts().get(0);
|
||||
String tldStr = tld.get().toString();
|
||||
if (isBlockedByBsa(label, Tld.get(tldStr), now)) {
|
||||
return ResponseOrException.of(
|
||||
new WhoisException(
|
||||
now,
|
||||
SC_NOT_FOUND,
|
||||
String.format(domainBlockedByBsaTemplate, domainName)));
|
||||
}
|
||||
|
||||
return ResponseOrException.of(
|
||||
new WhoisException(now, SC_NOT_FOUND, ERROR_PREFIX + " not found."));
|
||||
});
|
||||
return result.returnOrThrow();
|
||||
}
|
||||
|
||||
private Optional<WhoisResponse> getResponse(InternetDomainName domainName, DateTime now) {
|
||||
Optional<Domain> domainResource =
|
||||
cached
|
||||
? loadByForeignKeyCached(Domain.class, domainName.toString(), now)
|
||||
@@ -49,4 +99,29 @@ public class DomainLookupCommand extends DomainOrHostLookupCommand {
|
||||
return domainResource.map(
|
||||
domain -> new DomainWhoisResponse(domain, fullOutput, whoisRedactedEmailText, now));
|
||||
}
|
||||
|
||||
@AutoValue
|
||||
abstract static class ResponseOrException {
|
||||
|
||||
abstract Optional<WhoisResponse> whoisResponse();
|
||||
|
||||
abstract Optional<WhoisException> exception();
|
||||
|
||||
WhoisResponse returnOrThrow() throws WhoisException {
|
||||
Verify.verify(
|
||||
whoisResponse().isPresent() || exception().isPresent(),
|
||||
"Response and exception must not both be missing.");
|
||||
return whoisResponse().orElseThrow(() -> exception().get());
|
||||
}
|
||||
|
||||
static ResponseOrException of(WhoisResponse response) {
|
||||
return new AutoValue_DomainLookupCommand_ResponseOrException(
|
||||
Optional.of(response), Optional.empty());
|
||||
}
|
||||
|
||||
static ResponseOrException of(WhoisException exception) {
|
||||
return new AutoValue_DomainLookupCommand_ResponseOrException(
|
||||
Optional.empty(), Optional.of(exception));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
// Copyright 2017 The Nomulus Authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package google.registry.whois;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static google.registry.model.tld.Tlds.findTldForName;
|
||||
import static google.registry.model.tld.Tlds.getTlds;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.net.InternetDomainName;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Represents a WHOIS lookup on a domain name (i.e. SLD) or a nameserver. */
|
||||
public abstract class DomainOrHostLookupCommand implements WhoisCommand {
|
||||
|
||||
@VisibleForTesting final InternetDomainName domainOrHostName;
|
||||
|
||||
private final String errorPrefix;
|
||||
|
||||
DomainOrHostLookupCommand(InternetDomainName domainName, String errorPrefix) {
|
||||
this.errorPrefix = errorPrefix;
|
||||
this.domainOrHostName = checkNotNull(domainName, "domainOrHostName");
|
||||
}
|
||||
|
||||
@Override
|
||||
public final WhoisResponse executeQuery(final DateTime now) throws WhoisException {
|
||||
Optional<InternetDomainName> tld = findTldForName(domainOrHostName);
|
||||
// Google Registry Policy: Do not return records under TLDs for which we're not authoritative.
|
||||
if (tld.isPresent() && getTlds().contains(tld.get().toString())) {
|
||||
final Optional<WhoisResponse> response = getResponse(domainOrHostName, now);
|
||||
if (response.isPresent()) {
|
||||
return response.get();
|
||||
}
|
||||
}
|
||||
throw new WhoisException(now, SC_NOT_FOUND, errorPrefix + " not found.");
|
||||
}
|
||||
|
||||
/** Renders a response record, provided its successfully retrieved entity. */
|
||||
protected abstract Optional<WhoisResponse> getResponse(
|
||||
InternetDomainName domainName, DateTime now);
|
||||
}
|
||||
@@ -14,26 +14,47 @@
|
||||
|
||||
package google.registry.whois;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static google.registry.model.EppResourceUtils.loadByForeignKey;
|
||||
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
|
||||
import static google.registry.model.tld.Tlds.findTldForName;
|
||||
import static google.registry.model.tld.Tlds.getTlds;
|
||||
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.net.InternetDomainName;
|
||||
import google.registry.model.host.Host;
|
||||
import java.util.Optional;
|
||||
import org.joda.time.DateTime;
|
||||
|
||||
/** Represents a WHOIS lookup on a nameserver based on its hostname. */
|
||||
public class NameserverLookupByHostCommand extends DomainOrHostLookupCommand {
|
||||
public class NameserverLookupByHostCommand implements WhoisCommand {
|
||||
|
||||
private static final String ERROR_PREFIX = "Nameserver";
|
||||
|
||||
@VisibleForTesting final InternetDomainName hostName;
|
||||
|
||||
boolean cached;
|
||||
|
||||
NameserverLookupByHostCommand(InternetDomainName hostName, boolean cached) {
|
||||
super(hostName, "Nameserver");
|
||||
this.hostName = checkNotNull(hostName, "hostName");
|
||||
this.cached = cached;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Optional<WhoisResponse> getResponse(InternetDomainName hostName, DateTime now) {
|
||||
public final WhoisResponse executeQuery(final DateTime now) throws WhoisException {
|
||||
Optional<InternetDomainName> tld = findTldForName(hostName);
|
||||
// Google Registry Policy: Do not return records under TLDs for which we're not authoritative.
|
||||
if (tld.isPresent() && getTlds().contains(tld.get().toString())) {
|
||||
final Optional<WhoisResponse> response = getResponse(hostName, now);
|
||||
if (response.isPresent()) {
|
||||
return response.get();
|
||||
}
|
||||
}
|
||||
throw new WhoisException(now, SC_NOT_FOUND, ERROR_PREFIX + " not found.");
|
||||
}
|
||||
|
||||
private Optional<WhoisResponse> getResponse(InternetDomainName hostName, DateTime now) {
|
||||
Optional<Host> host =
|
||||
cached
|
||||
? loadByForeignKeyCached(Host.class, hostName.toString(), now)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user