// 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.net.MediaType.PLAIN_TEXT_UTF_8; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assert_; import static google.registry.testing.DatastoreHelper.createTlds; import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.testing.DatastoreHelper.persistSimpleResources; import static google.registry.testing.FullFieldsTestEntityHelper.makeContactResource; import static google.registry.testing.FullFieldsTestEntityHelper.makeDomainResource; import static google.registry.testing.FullFieldsTestEntityHelper.makeHostResource; import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrar; import static google.registry.testing.FullFieldsTestEntityHelper.makeRegistrarContacts; import static google.registry.whois.WhoisHelper.loadWhoisTestFile; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import google.registry.model.contact.ContactResource; import google.registry.model.ofy.Ofy; import google.registry.model.registrar.Registrar; import google.registry.model.registry.Registry; import google.registry.testing.AppEngineRule; import google.registry.testing.FakeClock; import google.registry.testing.FakeResponse; import google.registry.testing.InjectRule; import google.registry.whois.WhoisMetrics.WhoisMetric; import java.io.IOException; import java.io.Reader; import org.joda.time.DateTime; import org.joda.time.Duration; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Unit tests for {@link WhoisHttpServer}. * * <p>This class should be limited to testing the HTTP interface, as the bulk of the WHOIS testing * can be found in {@link WhoisServerTest}. */ @RunWith(JUnit4.class) public class WhoisHttpServerTest { @Rule public final AppEngineRule appEngine = AppEngineRule.builder().withDatastore().build(); @Rule public final InjectRule inject = new InjectRule(); private final FakeResponse response = new FakeResponse(); private final FakeClock clock = new FakeClock(DateTime.parse("2009-06-29T20:13:00Z")); private WhoisHttpServer newWhoisHttpServer(String pathInfo) { WhoisHttpServer whoisServer = new WhoisHttpServer(); whoisServer.clock = clock; whoisServer.expires = Duration.standardHours(1); whoisServer.requestPath = WhoisHttpServer.PATH + pathInfo; whoisServer.response = response; whoisServer.whoisReader = new WhoisReader(new WhoisCommandFactory()); whoisServer.whoisMetrics = new WhoisMetrics(); whoisServer.metricBuilder = WhoisMetric.builderForRequest(clock); whoisServer.disclaimer = "Doodle Disclaimer"; return whoisServer; } @Before public void setUp() throws Exception { createTlds("lol", "xn--q9jyb4c", "1.test"); inject.setStaticField(Ofy.class, "clock", clock); } @After public void after() throws Exception { // Extra precaution to ensure HTML escaping is working securely. assertThat(response.getPayload()).doesNotContain("<script>"); } @Test public void testRun_emptyQuery_returns400BadRequestWithPlainTextOutput() throws Exception { newWhoisHttpServer("").run(); assertThat(response.getStatus()).isEqualTo(400); assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8); assertThat(response.getPayload()).isEqualTo(loadWhoisTestFile("whois_server_no_command.txt")); } @Test public void testRun_badUrlEncoding_returns400BadRequestWithPlainTextOutput() throws Exception { newWhoisHttpServer("nic.%u307F%u3093%u306A").run(); assertThat(response.getStatus()).isEqualTo(400); assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8); assertThat(response.getPayload()) .isEqualTo(loadWhoisTestFile("whois_server_malformed_path.txt")); } @Test public void testRun_domainNotFound_returns404StatusAndPlainTextResponse() throws Exception { newWhoisHttpServer("/domain/cat.lol").run(); assertThat(response.getStatus()).isEqualTo(404); assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8); assertThat(response.getPayload()) .isEqualTo(loadWhoisTestFile("whois_server_domain_not_found.txt")); } // todo (b/27378695): reenable or delete this test @Ignore @Test public void testRun_domainInTestTld_isConsideredNotFound() throws Exception { persistResource(Registry.get("lol").asBuilder().setTldType(Registry.TldType.TEST).build()); Registrar registrar = persistResource(makeRegistrar( "evilregistrar", "Yes Virginia <script>", Registrar.State.ACTIVE)); persistResource(makeDomainResource( "cat.lol", persistResource(makeContactResource("5372808-ERL", "Goblin Market", "lol@cat.lol")), persistResource(makeContactResource("5372808-IRL", "Santa Claus", "BOFH@cat.lol")), persistResource(makeContactResource("5372808-TRL", "The Raven", "bog@cat.lol")), persistResource(makeHostResource("ns1.cat.lol", "1.2.3.4")), persistResource(makeHostResource("ns2.cat.lol", "bad:f00d:cafe::15:beef")), registrar)); persistSimpleResources(makeRegistrarContacts(registrar)); newWhoisHttpServer("/domain/cat.lol").run(); assertThat(response.getStatus()).isEqualTo(404); assertThat(response.getContentType()).isEqualTo(PLAIN_TEXT_UTF_8); assertThat(response.getPayload()) .isEqualTo(loadWhoisTestFile("whois_server_domain_not_found.txt")); } @Test public void testRun_domainQueryIdn_works() throws Exception { Registrar registrar = persistResource(makeRegistrar( "evilregistrar", "Yes Virginia <script>", Registrar.State.ACTIVE)); persistResource(makeDomainResource( "cat.みんな", persistResource(makeContactResource("5372808-ERL", "(◕‿◕)", "lol@cat.みんな")), persistResource(makeContactResource("5372808-IRL", "Santa Claus", "BOFH@cat.みんな")), persistResource(makeContactResource("5372808-TRL", "The Raven", "bog@cat.みんな")), persistResource(makeHostResource("ns1.cat.みんな", "1.2.3.4")), persistResource(makeHostResource("ns2.cat.みんな", "bad:f00d:cafe::15:beef")), registrar)); persistSimpleResources(makeRegistrarContacts(registrar)); newWhoisHttpServer("/domain/cat.みんな").run(); assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getPayload()).isEqualTo(loadWhoisTestFile("whois_server_idn_utf8.txt")); } @Test public void testRun_wickedLineFeedForgeryInDatastore_crlfSubstitutedWithSpace() throws Exception { String evilName = "Eric\r\nSchmidt"; ContactResource trl = persistResource( makeContactResource("5372808-TRL", evilName, "bog@cat.みんな")); persistResource(makeDomainResource( "cat.みんな", trl, trl, trl, persistResource(makeHostResource("ns1.cat.みんな", "1.2.3.4")), persistResource(makeHostResource("ns2.cat.みんな", "bad:f00d:cafe::15:beef")), persistResource( makeRegistrar("example", "Example Registrar", Registrar.State.ACTIVE)))); newWhoisHttpServer("/domain/cat.みんな").run(); assertThat(response.getPayload()).contains("Eric Schmidt"); } @Test public void testRun_domainOnly_works() throws Exception { persistResource(makeDomainResource( "cat.みんな", persistResource(makeContactResource("5372808-ERL", "(◕‿◕)", "lol@cat.みんな")), persistResource(makeContactResource("5372808-IRL", "Operator", "BOFH@cat.みんな")), persistResource(makeContactResource("5372808-TRL", "Eric Schmidt", "bog@cat.みんな")), persistResource(makeHostResource("ns1.cat.みんな", "1.2.3.4")), persistResource(makeHostResource("ns2.cat.みんな", "bad:f00d:cafe::15:beef")), persistResource(makeRegistrar("example", "Example Registrar", Registrar.State.ACTIVE)))); newWhoisHttpServer("cat.みんな").run(); assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getPayload()).contains("Domain Name: cat.みんな\r\n"); } @Test public void testRun_maliciousHtmlInDatastore_getsPurged() throws Exception { persistResource(makeDomainResource("cat.みんな", persistResource(makeContactResource("5372808-ERL", "(◕‿◕)", "lol@cat.みんな")), persistResource(makeContactResource("5372808-IRL", "Operator", "BOFH@cat.みんな")), persistResource( makeContactResource("5372808-TRL", "<script>alert('lol');</script>", "bog@cat.みんな")), persistResource(makeHostResource("ns1.cat.みんな", "1.2.3.4")), persistResource(makeHostResource("ns2.cat.みんな", "bad:f00d:cafe::15:beef")), persistResource(makeRegistrar("example", "Example Registrar", Registrar.State.ACTIVE)))); newWhoisHttpServer("cat.みんな").run(); assertThat(response.getPayload()).doesNotContain("<script>"); } @Test public void testRun_hostnameOnly_works() throws Exception { persistResource(makeHostResource("ns1.cat.みんな", "1.2.3.4")); newWhoisHttpServer("ns1.cat.みんな").run(); assertThat(response.getPayload()).contains("Server Name: ns1.cat.みんな\r\n"); } @Test public void testRun_domainQueryPunycode_works() throws Exception { Registrar registrar = persistResource(makeRegistrar( "evilregistrar", "Yes Virginia <script>", Registrar.State.ACTIVE)); persistResource(makeDomainResource( "cat.みんな", persistResource(makeContactResource("5372808-ERL", "(◕‿◕)", "lol@cat.みんな")), persistResource(makeContactResource("5372808-IRL", "Santa Claus", "BOFH@cat.みんな")), persistResource(makeContactResource("5372808-TRL", "The Raven", "bog@cat.みんな")), persistResource(makeHostResource("ns1.cat.みんな", "1.2.3.4")), persistResource(makeHostResource("ns2.cat.みんな", "bad:f00d:cafe::15:beef")), registrar)); persistSimpleResources(makeRegistrarContacts(registrar)); newWhoisHttpServer("/domain/cat.xn--q9jyb4c").run(); assertThat(response.getPayload()).isEqualTo(loadWhoisTestFile("whois_server_idn_utf8.txt")); } @Test public void testRun_nameserverQuery_works() throws Exception { persistResource(makeHostResource("ns1.cat.lol", "1.2.3.4")); newWhoisHttpServer("/nameserver/ns1.cat.lol").run(); assertThat(response.getPayload()).isEqualTo(loadWhoisTestFile("whois_server_nameserver.txt")); } // todo (b/27378695): reenable or delete this test @Ignore @Test public void testRun_nameserverQueryInTestTld_notFound() throws Exception { persistResource(makeHostResource("ns1.cat.lol", "1.2.3.4")); newWhoisHttpServer("/nameserver/ns1.cat.lol").run(); assertThat(response.getPayload()).isEqualTo(loadWhoisTestFile("whois_server_nameserver.txt")); } @Test public void testRun_lastUpdateTimestamp_isPresentInResponse() throws Exception { clock.setTo(DateTime.parse("2020-07-12T23:52:43Z")); persistResource(makeHostResource("ns1.cat.lol", "1.2.3.4")); newWhoisHttpServer("/nameserver/ns1.cat.lol").run(); assertThat(response.getPayload()) .contains(">>> Last update of WHOIS database: 2020-07-12T23:52:43Z <<<"); } @Test public void testRun_nameserverQueryIdn_works() throws Exception { persistResource(makeHostResource("ns1.cat.みんな", "1.2.3.4")); newWhoisHttpServer("/nameserver/ns1.cat.みんな").run(); assertThat(response.getPayload()).contains("ns1.cat.みんな"); assertThat(response.getPayload()).contains("1.2.3.4"); } @Test public void testRun_nameserverQueryPunycode_works() throws Exception { persistResource(makeHostResource("ns1.cat.みんな", "1.2.3.4")); newWhoisHttpServer("/nameserver/ns1.cat.xn--q9jyb4c").run(); assertThat(response.getPayload()).contains("ns1.cat.みんな"); assertThat(response.getPayload()).contains("1.2.3.4"); } @Test public void testRun_trailingSlashInPath_getsIgnored() throws Exception { persistResource(makeHostResource("ns1.cat.みんな", "1.2.3.4")); newWhoisHttpServer("/nameserver/ns1.cat.xn--q9jyb4c/").run(); assertThat(response.getPayload()).contains("ns1.cat.みんな"); assertThat(response.getPayload()).contains("1.2.3.4"); } @Test public void testRun_uppercaseDomain_ignoresCasing() throws Exception { persistResource(makeDomainResource( "cat.lol", persistResource(makeContactResource("5372808-ERL", "Peter Murphy", "lol@cat.lol")), persistResource(makeContactResource("5372808-IRL", "Operator", "BOFH@cat.lol")), persistResource(makeContactResource("5372808-TRL", "Eric Schmidt", "bog@cat.lol")), persistResource(makeHostResource("ns1.cat.lol", "1.2.3.4")), persistResource(makeHostResource("ns2.cat.lol", "bad:f00d:cafe::15:beef")), persistResource(makeRegistrar("example", "Example Registrar", Registrar.State.ACTIVE)))); newWhoisHttpServer("/domain/cat.LOL").run(); assertThat(response.getPayload()).contains("Domain Name: cat.lol\r\n"); } @Test public void testRun_hairyPath_getsDecoded() throws Exception { persistResource(makeDomainResource( "cat.lol", persistResource(makeContactResource("5372808-ERL", "Peter Murphy", "lol@cat.lol")), persistResource(makeContactResource("5372808-IRL", "Operator", "BOFH@cat.lol")), persistResource(makeContactResource("5372808-TRL", "Eric Schmidt", "bog@cat.lol")), persistResource(makeHostResource("ns1.cat.lol", "1.2.3.4")), persistResource(makeHostResource("ns2.cat.lol", "bad:f00d:cafe::15:beef")), persistResource(makeRegistrar("example", "Example Registrar", Registrar.State.ACTIVE)))); // python -c "print ''.join('%' + hex(ord(c))[2:] for c in 'cat.lol')" newWhoisHttpServer("/domain/%63%61%74%2e%6c%6f%6c").run(); assertThat(response.getPayload()).contains("Domain Name: cat.lol\r\n"); } @Test public void testRun_registrarLookup_works() throws Exception { Registrar registrar = persistResource( makeRegistrar("example", "Example Registrar, Inc.", Registrar.State.ACTIVE)); persistSimpleResources(makeRegistrarContacts(registrar)); // Notice the partial search without "inc". newWhoisHttpServer("/registrar/Example%20Registrar").run(); assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getPayload()).isEqualTo(loadWhoisTestFile("whois_server_registrar.txt")); } @Test public void testRun_registrarLookupInPendingState_returnsNotFound() throws Exception { Registrar registrar = persistResource( makeRegistrar("example", "Example Registrar, Inc.", Registrar.State.PENDING)); persistSimpleResources(makeRegistrarContacts(registrar)); newWhoisHttpServer("/registrar/Example%20Registrar,%20Inc.").run(); assertThat(response.getStatus()).isEqualTo(404); assertThat(response.getPayload()) .isEqualTo(loadWhoisTestFile("whois_server_registrar_not_found.txt")); } @Test public void testRun_registrarLookupWithTestType_returnsNotFound() throws Exception { Registrar registrar = persistResource( makeRegistrar("example", "Example Registrar, Inc.", Registrar.State.ACTIVE) .asBuilder().setType(Registrar.Type.TEST).setIanaIdentifier(null).build()); persistSimpleResources(makeRegistrarContacts(registrar)); newWhoisHttpServer("/registrar/Example%20Registrar,%20Inc.").run(); assertThat(response.getStatus()).isEqualTo(404); assertThat(response.getPayload()) .isEqualTo(loadWhoisTestFile("whois_server_registrar_not_found.txt")); } @Test public void testRun_metricsLoggedForSuccessfulCommand() throws Exception { persistResource(makeHostResource("ns1.cat.lol", "1.2.3.4")); WhoisHttpServer server = newWhoisHttpServer("/nameserver/ns1.cat.lol"); server.whoisMetrics = mock(WhoisMetrics.class); server.run(); WhoisMetric expected = WhoisMetric.builderForRequest(clock) .setCommandName("NameserverLookupByHost") .setNumResults(1) .setStatus(SC_OK) .build(); verify(server.whoisMetrics).recordWhoisMetric(eq(expected)); } @Test public void testRun_metricsLoggedForUnsuccessfulCommand() throws Exception { WhoisHttpServer server = newWhoisHttpServer("nic.%u307F%u3093%u306A"); server.whoisMetrics = mock(WhoisMetrics.class); server.run(); WhoisMetric expected = WhoisMetric.builderForRequest(clock).setNumResults(0).setStatus(SC_BAD_REQUEST).build(); verify(server.whoisMetrics).recordWhoisMetric(eq(expected)); } @Test public void testRun_metricsLoggedForInternalServerError() throws Exception { persistResource(makeHostResource("ns1.cat.lol", "1.2.3.4")); WhoisHttpServer server = newWhoisHttpServer("ns1.cat.lol"); server.whoisReader = mock(WhoisReader.class); when(server.whoisReader.readCommand(any(Reader.class), any(DateTime.class))) .thenThrow(new IOException("missing cat interface")); server.whoisMetrics = mock(WhoisMetrics.class); try { server.run(); assert_().fail("Should have thrown RuntimeException"); } catch (RuntimeException e) { assertThat(e.getCause().getMessage()).isEqualTo("missing cat interface"); } WhoisMetric expected = WhoisMetric.builderForRequest(clock) .setNumResults(0) .setStatus(SC_INTERNAL_SERVER_ERROR) .build(); verify(server.whoisMetrics).recordWhoisMetric(eq(expected)); } }