// 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.ui.server; import static com.google.common.collect.Range.atLeast; import static com.google.common.collect.Range.atMost; import static com.google.common.collect.Range.closed; import static google.registry.util.DomainNameUtils.canonicalizeDomainName; import com.google.common.base.Ascii; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.net.InternetDomainName; import com.google.re2j.Pattern; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarAddress; import google.registry.model.registrar.RegistrarContact; import google.registry.ui.forms.FormField; import google.registry.ui.forms.FormFieldException; import google.registry.ui.forms.FormFields; import google.registry.util.CidrAddressBlock; import google.registry.util.X509Utils; import java.security.cert.CertificateParsingException; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; /** Form fields for validating input for the {@code Registrar} class. */ public final class RegistrarFormFields { public static final Pattern BASE64_PATTERN = Pattern.compile("[+/a-zA-Z0-9]*"); public static final Pattern ASCII_PATTERN = Pattern.compile("[[:ascii:]]*"); public static final String ASCII_ERROR = "Please only use ASCII-US characters."; private static final Function CIDR_TRANSFORM = input -> { try { return input != null ? CidrAddressBlock.create(input) : null; } catch (IllegalArgumentException e) { throw new FormFieldException("Not a valid CIDR notation IP-address block.", e); } }; private static final Function HOSTNAME_TRANSFORM = input -> { if (input == null) { return null; } if (!InternetDomainName.isValid(input)) { throw new FormFieldException("Not a valid hostname."); } return canonicalizeDomainName(input); }; public static final FormField NAME_FIELD = FormFields.NAME.asBuilderNamed("registrarName") .required() .build(); public static final FormField EMAIL_ADDRESS_FIELD = FormFields.EMAIL.asBuilderNamed("emailAddress") .matches(ASCII_PATTERN, ASCII_ERROR) .required() .build(); public static final FormField ICANN_REFERRAL_EMAIL_FIELD = FormFields.EMAIL.asBuilderNamed("icannReferralEmail") .matches(ASCII_PATTERN, ASCII_ERROR) .required() .build(); public static final FormField PHONE_NUMBER_FIELD = FormFields.PHONE_NUMBER.asBuilder() .matches(ASCII_PATTERN, ASCII_ERROR) .build(); public static final FormField FAX_NUMBER_FIELD = FormFields.PHONE_NUMBER.asBuilderNamed("faxNumber") .matches(ASCII_PATTERN, ASCII_ERROR) .build(); public static final FormField STATE_FIELD = FormField.named("state") .emptyToNull() .asEnum(Registrar.State.class) .build(); public static final FormField, Set> ALLOWED_TLDS_FIELD = FormFields.LABEL.asBuilderNamed("allowedTlds") .asSet() .build(); public static final FormField WHOIS_SERVER_FIELD = FormFields.LABEL.asBuilderNamed("whoisServer") .transform(HOSTNAME_TRANSFORM) .build(); public static final FormField BLOCK_PREMIUM_NAMES_FIELD = FormField.named("blockPremiumNames", Boolean.class) .build(); public static final FormField DRIVE_FOLDER_ID_FIELD = FormFields.XS_NORMALIZED_STRING.asBuilderNamed("driveFolderId") .build(); public static final FormField CLIENT_CERTIFICATE_HASH_FIELD = FormField.named("clientCertificateHash") .emptyToNull() .matches(BASE64_PATTERN, "Field must contain a base64 value.") .range(closed(1, 255)) .build(); private static final FormField X509_PEM_CERTIFICATE = FormField.named("certificate") .emptyToNull() .transform( input -> { if (input == null) { return null; } try { X509Utils.loadCertificate(input); } catch (CertificateParsingException e) { throw new FormFieldException("Invalid X.509 PEM certificate"); } return input; }) .build(); public static final FormField CLIENT_CERTIFICATE_FIELD = X509_PEM_CERTIFICATE.asBuilderNamed("clientCertificate").build(); public static final FormField FAILOVER_CLIENT_CERTIFICATE_FIELD = X509_PEM_CERTIFICATE.asBuilderNamed("failoverClientCertificate").build(); public static final FormField BILLING_IDENTIFIER_FIELD = FormField.named("billingIdentifier", Long.class) .range(atLeast(1)) .build(); public static final FormField IANA_IDENTIFIER_FIELD = FormField.named("ianaIdentifier", Long.class) .range(atLeast(1)) .build(); public static final FormField URL_FIELD = FormFields.MIN_TOKEN.asBuilderNamed("url") .build(); public static final FormField REFERRAL_URL_FIELD = FormFields.MIN_TOKEN.asBuilderNamed("referralUrl") .build(); public static final FormField, List> IP_ADDRESS_WHITELIST_FIELD = FormField.named("ipAddressWhitelist") .emptyToNull() .transform(CidrAddressBlock.class, CIDR_TRANSFORM) .asList() .build(); public static final FormField PASSWORD_FIELD = FormFields.PASSWORD.asBuilderNamed("password") .build(); public static final FormField PHONE_PASSCODE_FIELD = FormField.named("phonePasscode") .emptyToNull() .matches(Registrar.PHONE_PASSCODE_PATTERN) .build(); public static final FormField CONTACT_NAME_FIELD = FormFields.NAME.asBuilderNamed("name") .required() .build(); public static final FormField CONTACT_EMAIL_ADDRESS_FIELD = FormFields.EMAIL.asBuilderNamed("emailAddress") .required() .build(); public static final FormField CONTACT_VISIBLE_IN_WHOIS_AS_ADMIN_FIELD = FormField.named("visibleInWhoisAsAdmin", Boolean.class) .build(); public static final FormField CONTACT_VISIBLE_IN_WHOIS_AS_TECH_FIELD = FormField.named("visibleInWhoisAsTech", Boolean.class) .build(); public static final FormField PHONE_AND_EMAIL_VISIBLE_IN_DOMAIN_WHOIS_AS_ABUSE_FIELD = FormField.named("visibleInDomainWhoisAsAbuse", Boolean.class).build(); public static final FormField CONTACT_PHONE_NUMBER_FIELD = FormFields.PHONE_NUMBER.asBuilder() .build(); public static final FormField CONTACT_FAX_NUMBER_FIELD = FormFields.PHONE_NUMBER.asBuilderNamed("faxNumber") .build(); public static final FormField CONTACT_GAE_USER_ID_FIELD = FormFields.NAME.asBuilderNamed("gaeUserId") .build(); public static final FormField> CONTACT_TYPES = FormField.named("types") .uppercased() .asEnum(RegistrarContact.Type.class) .asSet(Splitter.on(',').omitEmptyStrings().trimResults()) .build(); public static final Function, RegistrarContact.Builder> REGISTRAR_CONTACT_TRANSFORM = args -> { if (args == null) { return null; } RegistrarContact.Builder builder = new RegistrarContact.Builder(); CONTACT_NAME_FIELD.extractUntyped(args).ifPresent(builder::setName); CONTACT_EMAIL_ADDRESS_FIELD.extractUntyped(args).ifPresent(builder::setEmailAddress); CONTACT_VISIBLE_IN_WHOIS_AS_ADMIN_FIELD .extractUntyped(args) .ifPresent(builder::setVisibleInWhoisAsAdmin); CONTACT_VISIBLE_IN_WHOIS_AS_TECH_FIELD .extractUntyped(args) .ifPresent(builder::setVisibleInWhoisAsTech); PHONE_AND_EMAIL_VISIBLE_IN_DOMAIN_WHOIS_AS_ABUSE_FIELD .extractUntyped(args) .ifPresent(builder::setVisibleInDomainWhoisAsAbuse); CONTACT_PHONE_NUMBER_FIELD.extractUntyped(args).ifPresent(builder::setPhoneNumber); CONTACT_FAX_NUMBER_FIELD.extractUntyped(args).ifPresent(builder::setFaxNumber); CONTACT_TYPES.extractUntyped(args).ifPresent(builder::setTypes); CONTACT_GAE_USER_ID_FIELD.extractUntyped(args).ifPresent(builder::setGaeUserId); return builder; }; public static final FormField>, List> CONTACTS_FIELD = FormField.mapNamed("contacts") .transform(RegistrarContact.Builder.class, REGISTRAR_CONTACT_TRANSFORM) .asList() .build(); public static final FormField, List> I18N_STREET_FIELD = FormFields.XS_NORMALIZED_STRING.asBuilderNamed("street") .range(closed(1, 255)) .matches(ASCII_PATTERN, ASCII_ERROR) .asList() .range(closed(1, 3)) .required() .build(); public static final FormField, List> L10N_STREET_FIELD = FormFields.XS_NORMALIZED_STRING.asBuilderNamed("street") .range(closed(1, 255)) .asList() .range(closed(1, 3)) .required() .build(); public static final FormField I18N_CITY_FIELD = FormFields.NAME.asBuilderNamed("city") .matches(ASCII_PATTERN, ASCII_ERROR) .required() .build(); public static final FormField L10N_CITY_FIELD = FormFields.NAME.asBuilderNamed("city") .required() .build(); public static final FormField I18N_STATE_FIELD = FormFields.XS_NORMALIZED_STRING.asBuilderNamed("state") .range(atMost(255)) .matches(ASCII_PATTERN, ASCII_ERROR) .build(); public static final FormField L10N_STATE_FIELD = FormFields.XS_NORMALIZED_STRING.asBuilderNamed("state") .range(atMost(255)) .build(); public static final FormField I18N_ZIP_FIELD = FormFields.XS_TOKEN.asBuilderNamed("zip") .range(atMost(16)) .matches(ASCII_PATTERN, ASCII_ERROR) .build(); public static final FormField L10N_ZIP_FIELD = FormFields.XS_TOKEN.asBuilderNamed("zip") .range(atMost(16)) .build(); public static final FormField COUNTRY_CODE_FIELD = FormFields.COUNTRY_CODE.asBuilder() .required() .build(); public static final FormField, RegistrarAddress> L10N_ADDRESS_FIELD = FormField.mapNamed("localizedAddress") .transform(RegistrarAddress.class, newAddressTransform( L10N_STREET_FIELD, L10N_CITY_FIELD, L10N_STATE_FIELD, L10N_ZIP_FIELD)) .build(); private static Function, RegistrarAddress> newAddressTransform( final FormField, List> streetField, final FormField cityField, final FormField stateField, final FormField zipField) { return args -> { if (args == null) { return null; } RegistrarAddress.Builder builder = new RegistrarAddress.Builder(); String countryCode = COUNTRY_CODE_FIELD.extractUntyped(args).get(); builder.setCountryCode(countryCode); streetField .extractUntyped(args) .ifPresent(streets -> builder.setStreet(ImmutableList.copyOf(streets))); cityField.extractUntyped(args).ifPresent(builder::setCity); Optional stateFieldValue = stateField.extractUntyped(args); if (stateFieldValue.isPresent()) { String state = stateFieldValue.get(); if ("US".equals(countryCode)) { state = Ascii.toUpperCase(state); if (!StateCode.US_MAP.containsKey(state)) { throw new FormFieldException(stateField, "Unknown US state code."); } } builder.setState(state); } zipField.extractUntyped(args).ifPresent(builder::setZip); return builder.build(); }; } }