diff --git a/java/google/registry/tools/WhoisQueryCommand.java b/java/google/registry/tools/WhoisQueryCommand.java index 088c084e5..027d4bac4 100644 --- a/java/google/registry/tools/WhoisQueryCommand.java +++ b/java/google/registry/tools/WhoisQueryCommand.java @@ -36,11 +36,14 @@ final class WhoisQueryCommand implements RemoteApiCommand { description = "When set, output will be Unicode") private boolean unicode; + @Parameter(names = "--full_output", description = "When set, the full output will be displayed") + private boolean fullOutput; + @Inject Whois whois; @Override public void run() { - System.out.println(whois.lookup(Joiner.on(' ').join(mainParameters), unicode)); + System.out.println(whois.lookup(Joiner.on(' ').join(mainParameters), unicode, fullOutput)); } } diff --git a/java/google/registry/whois/DomainLookupCommand.java b/java/google/registry/whois/DomainLookupCommand.java index e73d81438..7932d8e0d 100644 --- a/java/google/registry/whois/DomainLookupCommand.java +++ b/java/google/registry/whois/DomainLookupCommand.java @@ -24,8 +24,11 @@ import org.joda.time.DateTime; /** Represents a WHOIS lookup on a domain name (i.e. SLD). */ public class DomainLookupCommand extends DomainOrHostLookupCommand { - public DomainLookupCommand(InternetDomainName domainName) { + private final boolean fullOutput; + + public DomainLookupCommand(InternetDomainName domainName, boolean fullOutput) { super(domainName, "Domain"); + this.fullOutput = fullOutput; } @Override @@ -33,6 +36,6 @@ public class DomainLookupCommand extends DomainOrHostLookupCommand { final DomainResource domainResource = loadByForeignKeyCached(DomainResource.class, domainName.toString(), now); return Optional.ofNullable( - domainResource == null ? null : new DomainWhoisResponse(domainResource, now)); + domainResource == null ? null : new DomainWhoisResponse(domainResource, fullOutput, now)); } } diff --git a/java/google/registry/whois/DomainWhoisResponse.java b/java/google/registry/whois/DomainWhoisResponse.java index 52bf284f6..719a00ef6 100644 --- a/java/google/registry/whois/DomainWhoisResponse.java +++ b/java/google/registry/whois/DomainWhoisResponse.java @@ -26,6 +26,8 @@ import google.registry.model.EppResource; import google.registry.model.contact.ContactPhoneNumber; import google.registry.model.contact.ContactResource; import google.registry.model.contact.PostalInfo; +import google.registry.model.domain.DesignatedContact; +import google.registry.model.domain.DesignatedContact.Type; import google.registry.model.domain.DomainResource; import google.registry.model.domain.GracePeriod; import google.registry.model.eppcommon.StatusValue; @@ -54,10 +56,14 @@ final class DomainWhoisResponse extends WhoisResponseImpl { /** Domain which was the target of this WHOIS command. */ private final DomainResource domain; + /** Whether the full WHOIS output is to be displayed. */ + private final boolean fullOutput; + /** Creates new WHOIS domain response on the given domain. */ - DomainWhoisResponse(DomainResource domain, DateTime timestamp) { + DomainWhoisResponse(DomainResource domain, boolean fullOutput, DateTime timestamp) { super(timestamp); this.domain = checkNotNull(domain, "domain"); + this.fullOutput = fullOutput; } @Override @@ -75,7 +81,7 @@ final class DomainWhoisResponse extends WhoisResponseImpl { .stream() .filter(RegistrarContact::getVisibleInDomainWhoisAsAbuse) .findFirst(); - String plaintext = + DomainEmitter domainEmitter = new DomainEmitter() .emitField( "Domain Name", @@ -98,19 +104,32 @@ final class DomainWhoisResponse extends WhoisResponseImpl { "Registrar Abuse Contact Phone", abuseContact.map(RegistrarContact::getPhoneNumber).orElse(null)) .emitStatusValues(domain.getStatusValues(), domain.getGracePeriods()) - .emitContact("Registrant", domain.getRegistrant(), preferUnicode) - .emitSet( - "Name Server", - domain.loadNameserverFullyQualifiedHostNames(), - hostName -> maybeFormatHostname(hostName, preferUnicode)) - .emitField( - "DNSSEC", isNullOrEmpty(domain.getDsData()) ? "unsigned" : "signedDelegation") - .emitWicfLink() - .emitLastUpdated(getTimestamp()) - .emitAwipMessage() - .emitFooter(disclaimer) - .toString(); - return WhoisResponseResults.create(plaintext, 1); + .emitContact( + "Registrant", Optional.of(domain.getRegistrant()), preferUnicode, fullOutput); + if (fullOutput) { + domainEmitter + .emitContact("Admin", getContactReference(Type.ADMIN), preferUnicode, fullOutput) + .emitContact("Tech", getContactReference(Type.TECH), preferUnicode, fullOutput) + .emitContact("Billing", getContactReference(Type.BILLING), preferUnicode, fullOutput); + } + domainEmitter + .emitSet( + "Name Server", + domain.loadNameserverFullyQualifiedHostNames(), + hostName -> maybeFormatHostname(hostName, preferUnicode)) + .emitField("DNSSEC", isNullOrEmpty(domain.getDsData()) ? "unsigned" : "signedDelegation") + .emitWicfLink() + .emitLastUpdated(getTimestamp()) + .emitAwipMessage() + .emitFooter(disclaimer); + return WhoisResponseResults.create(domainEmitter.toString(), 1); + } + + /** Returns the contact of the given type. */ + private Optional> getContactReference(Type type) { + Optional contactOfType = + domain.getContacts().stream().filter(d -> d.getType() == type).findFirst(); + return contactOfType.map(DesignatedContact::getContactKey); } /** Output emitter with logic for domains. */ @@ -127,14 +146,17 @@ final class DomainWhoisResponse extends WhoisResponseImpl { /** Emit the contact entry of the given type. */ DomainEmitter emitContact( - String contactType, @Nullable Key contact, boolean preferUnicode) { - if (contact == null) { + String contactType, + Optional> contact, + boolean preferUnicode, + boolean fullOutput) { + if (!contact.isPresent()) { return this; } // If we refer to a contact that doesn't exist, that's a bug. It means referential integrity // has somehow been broken. We skip the rest of this contact, but log it to hopefully bring it // someone's attention. - ContactResource contactResource = EppResource.loadCached(contact); + ContactResource contactResource = EppResource.loadCached(contact.get()); if (contactResource == null) { logger.severefmt( "(BUG) Broken reference found from domain %s to contact %s", @@ -146,9 +168,23 @@ final class DomainWhoisResponse extends WhoisResponseImpl { preferUnicode, contactResource.getLocalizedPostalInfo(), contactResource.getInternationalizedPostalInfo()); - if (postalInfo != null) { - emitFieldIfDefined(ImmutableList.of(contactType, "Organization"), postalInfo.getOrg()); - emitRegistrantAddress(contactType, postalInfo.getAddress()); + if (fullOutput) { + // If the full output is to be displayed, show all fields for all contact types. + // ICANN Consistent Labeling & Display policy requires that this be the ROID. + emitField(ImmutableList.of("Registry", contactType, "ID"), contactResource.getRepoId()); + if (postalInfo != null) { + emitFieldIfDefined(ImmutableList.of(contactType, "Name"), postalInfo.getName()); + emitFieldIfDefined(ImmutableList.of(contactType, "Organization"), postalInfo.getOrg()); + emitAddress(contactType, postalInfo.getAddress(), fullOutput); + } + emitPhone(contactType, "Phone", contactResource.getVoiceNumber()); + emitPhone(contactType, "Fax", contactResource.getFaxNumber()); + emitField(ImmutableList.of(contactType, "Email"), contactResource.getEmailAddress()); + } else { + if (postalInfo != null) { + emitFieldIfDefined(ImmutableList.of(contactType, "Organization"), postalInfo.getOrg()); + emitAddress(contactType, postalInfo.getAddress(), fullOutput); + } } return this; } diff --git a/java/google/registry/whois/RegistrarWhoisResponse.java b/java/google/registry/whois/RegistrarWhoisResponse.java index 4ad52b191..47ca4b40b 100644 --- a/java/google/registry/whois/RegistrarWhoisResponse.java +++ b/java/google/registry/whois/RegistrarWhoisResponse.java @@ -48,12 +48,13 @@ class RegistrarWhoisResponse extends WhoisResponseImpl { String plaintext = new RegistrarEmitter() .emitField("Registrar", registrar.getRegistrarName()) - .emitRegistrarAddress( + .emitAddress( null, chooseByUnicodePreference( preferUnicode, registrar.getLocalizedAddress(), - registrar.getInternationalizedAddress())) + registrar.getInternationalizedAddress()), + true) .emitPhonesAndEmail( registrar.getPhoneNumber(), registrar.getFaxNumber(), registrar.getEmailAddress()) .emitField("Registrar WHOIS Server", registrar.getWhoisServer()) diff --git a/java/google/registry/whois/Whois.java b/java/google/registry/whois/Whois.java index e25ad64fc..894f9542a 100644 --- a/java/google/registry/whois/Whois.java +++ b/java/google/registry/whois/Whois.java @@ -36,11 +36,11 @@ public final class Whois { } /** Performs a WHOIS lookup on a plaintext query string. */ - public String lookup(String query, boolean preferUnicode) { + public String lookup(String query, boolean preferUnicode, boolean fullOutput) { DateTime now = clock.nowUtc(); try { return whoisReader - .readCommand(new StringReader(query), now) + .readCommand(new StringReader(query), fullOutput, now) .executeQuery(now) .getResponse(preferUnicode, disclaimer) .plainTextOutput(); diff --git a/java/google/registry/whois/WhoisAction.java b/java/google/registry/whois/WhoisAction.java index c7836b46a..cc63fed00 100644 --- a/java/google/registry/whois/WhoisAction.java +++ b/java/google/registry/whois/WhoisAction.java @@ -80,7 +80,7 @@ public class WhoisAction implements Runnable { String responseText; final DateTime now = clock.nowUtc(); try { - final WhoisCommand command = whoisReader.readCommand(input, now); + final WhoisCommand command = whoisReader.readCommand(input, false, now); metricBuilder.setCommand(command); WhoisResponseResults results = retrier.callWithRetry( diff --git a/java/google/registry/whois/WhoisCommandFactory.java b/java/google/registry/whois/WhoisCommandFactory.java index 9a8ff1fa0..b5085396b 100644 --- a/java/google/registry/whois/WhoisCommandFactory.java +++ b/java/google/registry/whois/WhoisCommandFactory.java @@ -27,8 +27,8 @@ import java.net.InetAddress; public class WhoisCommandFactory { /** Returns a new {@link WhoisCommand} to perform a domain lookup on the specified domain name. */ - public WhoisCommand domainLookup(InternetDomainName domainName) { - return new DomainLookupCommand(domainName); + public WhoisCommand domainLookup(InternetDomainName domainName, boolean fullOutput) { + return new DomainLookupCommand(domainName, fullOutput); } /** diff --git a/java/google/registry/whois/WhoisHttpAction.java b/java/google/registry/whois/WhoisHttpAction.java index 64371d6a3..60dd7eedd 100644 --- a/java/google/registry/whois/WhoisHttpAction.java +++ b/java/google/registry/whois/WhoisHttpAction.java @@ -151,7 +151,7 @@ public final class WhoisHttpAction implements Runnable { String commandText = decode(JOINER.join(SLASHER.split(path.substring(PATH.length())))) + "\r\n"; DateTime now = clock.nowUtc(); - WhoisCommand command = whoisReader.readCommand(new StringReader(commandText), now); + WhoisCommand command = whoisReader.readCommand(new StringReader(commandText), false, now); metricBuilder.setCommand(command); sendResponse(SC_OK, command.executeQuery(now)); } catch (WhoisException e) { diff --git a/java/google/registry/whois/WhoisReader.java b/java/google/registry/whois/WhoisReader.java index 455046142..95f1e82c9 100644 --- a/java/google/registry/whois/WhoisReader.java +++ b/java/google/registry/whois/WhoisReader.java @@ -82,21 +82,22 @@ class WhoisReader { } /** - * Read a command from some source to produce a new instance of - * WhoisCommand. + * Read a command from some source to produce a new instance of WhoisCommand. * * @throws IOException If the command could not be read from the reader. * @throws WhoisException If the command could not be parsed as a WhoisCommand. */ - WhoisCommand readCommand(Reader reader, DateTime now) throws IOException, WhoisException { - return parseCommand(CharStreams.toString(checkNotNull(reader, "reader")), now); + WhoisCommand readCommand(Reader reader, boolean fullOutput, DateTime now) + throws IOException, WhoisException { + return parseCommand(CharStreams.toString(checkNotNull(reader, "reader")), fullOutput, now); } /** * Given a WHOIS command string, parse it into its command type and target string. See class level * comments for a full description of the command syntax accepted. */ - private WhoisCommand parseCommand(String command, DateTime now) throws WhoisException { + private WhoisCommand parseCommand(String command, boolean fullOutput, DateTime now) + throws WhoisException { // Split the string into tokens based on whitespace. List tokens = filterEmptyStrings(command.split("\\s")); if (tokens.isEmpty()) { @@ -115,8 +116,8 @@ class WhoisReader { // Try to parse the argument as a domain name. try { logger.infofmt("Attempting domain lookup command using domain name %s", tokens.get(1)); - return commandFactory.domainLookup(InternetDomainName.from( - canonicalizeDomainName(tokens.get(1)))); + return commandFactory.domainLookup( + InternetDomainName.from(canonicalizeDomainName(tokens.get(1))), fullOutput); } catch (IllegalArgumentException iae) { // If we can't interpret the argument as a host name, then return an error. throw new WhoisException(now, SC_BAD_REQUEST, String.format( @@ -194,7 +195,7 @@ class WhoisReader { // (SLD) and we should do a domain lookup on it. if (targetName.parent().equals(tld.get())) { logger.infofmt("Attempting domain lookup using %s as a domain name", targetName); - return commandFactory.domainLookup(targetName); + return commandFactory.domainLookup(targetName, fullOutput); } // The target is more than one level above the TLD, so we'll assume it's a nameserver. diff --git a/java/google/registry/whois/WhoisResponseImpl.java b/java/google/registry/whois/WhoisResponseImpl.java index d23e9df91..b6dca066f 100644 --- a/java/google/registry/whois/WhoisResponseImpl.java +++ b/java/google/registry/whois/WhoisResponseImpl.java @@ -134,24 +134,18 @@ abstract class WhoisResponseImpl implements WhoisResponse { return emitField(Joiner.on(' ').join(nameParts), value); } - /** Emit registrar address. */ - E emitRegistrarAddress(@Nullable String prefix, @Nullable Address address) { - prefix = isNullOrEmpty(prefix) ? "" : prefix + " "; - if (address != null) { - emitList(prefix + "Street", address.getStreet()); - emitField(prefix + "City", address.getCity()); - emitField(prefix + "State/Province", address.getState()); - emitField(prefix + "Postal Code", address.getZip()); - emitField(prefix + "Country", address.getCountryCode()); - } - return thisCastToDerived(); - } - - /** Emit registrant address. */ - E emitRegistrantAddress(@Nullable String prefix, @Nullable Address address) { + /** Emit a contact address. */ + E emitAddress(@Nullable String prefix, @Nullable Address address, boolean fullOutput) { prefix = isNullOrEmpty(prefix) ? "" : prefix + " "; if (address != null) { + if (fullOutput) { + emitList(prefix + "Street", address.getStreet()); + emitField(prefix + "City", address.getCity()); + } emitField(prefix + "State/Province", address.getState()); + if (fullOutput) { + emitField(prefix + "Postal Code", address.getZip()); + } emitField(prefix + "Country", address.getCountryCode()); } return thisCastToDerived(); diff --git a/javatests/google/registry/whois/DomainWhoisResponseTest.java b/javatests/google/registry/whois/DomainWhoisResponseTest.java index ffb7d0e65..136868ed2 100644 --- a/javatests/google/registry/whois/DomainWhoisResponseTest.java +++ b/javatests/google/registry/whois/DomainWhoisResponseTest.java @@ -255,7 +255,7 @@ public class DomainWhoisResponseTest { @Test public void getPlainTextOutputTest() { DomainWhoisResponse domainWhoisResponse = - new DomainWhoisResponse(domainResource, clock.nowUtc()); + new DomainWhoisResponse(domainResource, false, clock.nowUtc()); assertThat( domainWhoisResponse.getResponse( false, @@ -263,11 +263,22 @@ public class DomainWhoisResponseTest { .isEqualTo(WhoisResponseResults.create(loadFile("whois_domain.txt"), 1)); } + @Test + public void getPlainTextOutputTest_fullOutput() { + DomainWhoisResponse domainWhoisResponse = + new DomainWhoisResponse(domainResource, true, clock.nowUtc()); + assertThat( + domainWhoisResponse.getResponse( + false, + "Doodle Disclaimer\nI exist so that carriage return\nin disclaimer can be tested.")) + .isEqualTo(WhoisResponseResults.create(loadFile("whois_domain_full_output.txt"), 1)); + } + @Test public void addImplicitOkStatusTest() { DomainWhoisResponse domainWhoisResponse = new DomainWhoisResponse( - domainResource.asBuilder().setStatusValues(null).build(), clock.nowUtc()); + domainResource.asBuilder().setStatusValues(null).build(), false, clock.nowUtc()); assertThat( domainWhoisResponse .getResponse( diff --git a/javatests/google/registry/whois/WhoisActionTest.java b/javatests/google/registry/whois/WhoisActionTest.java index 83d142927..289e76ef4 100644 --- a/javatests/google/registry/whois/WhoisActionTest.java +++ b/javatests/google/registry/whois/WhoisActionTest.java @@ -554,7 +554,7 @@ public class WhoisActionTest { persistResource(makeHostResource("ns1.cat.lol", "1.2.3.4")); WhoisAction action = newWhoisAction("ns1.cat.lol"); action.whoisReader = mock(WhoisReader.class); - when(action.whoisReader.readCommand(any(Reader.class), any(DateTime.class))) + when(action.whoisReader.readCommand(any(Reader.class), eq(false), any(DateTime.class))) .thenThrow(new IOException("missing cat interface")); action.whoisMetrics = mock(WhoisMetrics.class); @@ -574,11 +574,15 @@ public class WhoisActionTest { persistResource(makeHostResource("ns1.cat.lol", "1.2.3.4")); WhoisAction action = newWhoisAction("ns1.cat.lol"); WhoisResponse expectedResponse = - action.whoisReader.readCommand(action.input, clock.nowUtc()).executeQuery(clock.nowUtc()); + action + .whoisReader + .readCommand(action.input, false, clock.nowUtc()) + .executeQuery(clock.nowUtc()); WhoisReader mockReader = mock(WhoisReader.class); WhoisCommand mockCommand = mock(WhoisCommand.class); - when(mockReader.readCommand(any(Reader.class), any(DateTime.class))).thenReturn(mockCommand); + when(mockReader.readCommand(any(Reader.class), eq(false), any(DateTime.class))) + .thenReturn(mockCommand); when(mockCommand.executeQuery(any(DateTime.class))) .thenThrow(new DatastoreFailureException("Expected transient exception #1")) .thenThrow(new DatastoreTimeoutException("Expected transient exception #2")) diff --git a/javatests/google/registry/whois/WhoisHttpActionTest.java b/javatests/google/registry/whois/WhoisHttpActionTest.java index 5dd84ec0f..7d82c37da 100644 --- a/javatests/google/registry/whois/WhoisHttpActionTest.java +++ b/javatests/google/registry/whois/WhoisHttpActionTest.java @@ -358,7 +358,7 @@ public class WhoisHttpActionTest { persistResource(makeHostResource("ns1.cat.lol", "1.2.3.4")); WhoisHttpAction action = newWhoisHttpAction("ns1.cat.lol"); action.whoisReader = mock(WhoisReader.class); - when(action.whoisReader.readCommand(any(Reader.class), any(DateTime.class))) + when(action.whoisReader.readCommand(any(Reader.class), eq(false), any(DateTime.class))) .thenThrow(new IOException("missing cat interface")); action.whoisMetrics = mock(WhoisMetrics.class); diff --git a/javatests/google/registry/whois/WhoisReaderTest.java b/javatests/google/registry/whois/WhoisReaderTest.java index bc5acc3b5..337655032 100644 --- a/javatests/google/registry/whois/WhoisReaderTest.java +++ b/javatests/google/registry/whois/WhoisReaderTest.java @@ -48,7 +48,7 @@ public class WhoisReaderTest { T readCommand(String commandStr) throws Exception { return (T) new WhoisReader(new WhoisCommandFactory()) - .readCommand(new StringReader(commandStr), clock.nowUtc()); + .readCommand(new StringReader(commandStr), false, clock.nowUtc()); } void assertLoadsExampleTld(String commandString) throws Exception { diff --git a/javatests/google/registry/whois/testdata/whois_domain_full_output.txt b/javatests/google/registry/whois/testdata/whois_domain_full_output.txt new file mode 100644 index 000000000..2f3e25843 --- /dev/null +++ b/javatests/google/registry/whois/testdata/whois_domain_full_output.txt @@ -0,0 +1,66 @@ +Domain Name: example.tld +Registry Domain ID: 3-TLD +Registrar WHOIS Server: whois.nic.fakewhois.example +Registrar URL: http://my.fake.url +Updated Date: 2009-05-29T20:13:00Z +Creation Date: 2000-10-08T00:45:00Z +Registry Expiry Date: 2010-10-08T00:44:59Z +Registrar: New Registrar +Registrar IANA ID: 5555555 +Registrar Abuse Contact Email: jakedoe@theregistrar.com +Registrar Abuse Contact Phone: +1.2125551216 +Domain Status: addPeriod https://icann.org/epp#addPeriod +Domain Status: clientDeleteProhibited https://icann.org/epp#clientDeleteProhibited +Domain Status: clientRenewProhibited https://icann.org/epp#clientRenewProhibited +Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited +Domain Status: serverUpdateProhibited https://icann.org/epp#serverUpdateProhibited +Domain Status: transferPeriod https://icann.org/epp#transferPeriod +Registry Registrant ID: 4-TLD +Registrant Name: EXAMPLE REGISTRANT +Registrant Organization: Tom & Jerry Corp. +Registrant Street: 123 EXAMPLE STREET +Registrant City: ANYTOWN +Registrant State/Province: AP +Registrant Postal Code: A1A1A1 +Registrant Country: EX +Registrant Phone: +1.5555551212 +Registrant Phone Ext: 1234 +Registrant Fax: +1.5555551213 +Registrant Fax Ext: 4321 +Registrant Email: EMAIL@EXAMPLE.tld +Registry Admin ID: 5-TLD +Admin Name: EXAMPLE REGISTRANT ADMINISTRATIVE +Admin Organization: EXAMPLE REGISTRANT ORGANIZATION +Admin Street: 123 EXAMPLE STREET +Admin City: ANYTOWN +Admin State/Province: AP +Admin Postal Code: A1A1A1 +Admin Country: EX +Admin Phone: +1.5555551212 +Admin Phone Ext: 1234 +Admin Fax: +1.5555551213 +Admin Email: EMAIL@EXAMPLE.tld +Registry Tech ID: 6-TLD +Tech Name: EXAMPLE REGISTRAR TECHNICAL +Tech Organization: EXAMPLE REGISTRAR LLC +Tech Street: 123 EXAMPLE STREET +Tech City: ANYTOWN +Tech State/Province: AP +Tech Postal Code: A1A1A1 +Tech Country: EX +Tech Phone: +1.1235551234 +Tech Phone Ext: 1234 +Tech Fax: +1.5555551213 +Tech Fax Ext: 93 +Tech Email: EMAIL@EXAMPLE.tld +Name Server: ns01.exampleregistrar.tld +Name Server: ns02.exampleregistrar.tld +DNSSEC: signedDelegation +URL of the ICANN Whois Inaccuracy Complaint Form: https://www.icann.org/wicf/ +>>> Last update of WHOIS database: 2009-05-29T20:15:00Z <<< + +For more information on Whois status codes, please visit https://icann.org/epp + +Doodle Disclaimer +I exist so that carriage return +in disclaimer can be tested.