/* * Copyright 2015-present Facebook, Inc. * * 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 com.facebook.buck.testutil.integration; import static java.nio.charset.StandardCharsets.UTF_16; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import com.google.common.base.Preconditions; import java.io.IOException; import java.net.InetAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.DefaultHandler; import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.server.handler.ResourceHandler; import org.eclipse.jetty.util.log.JavaUtilLog; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.StdErrLog; /** Lightweight wrapper around an httpd to make testing using an httpd nicer. */ public class HttpdForTests implements AutoCloseable { private final HandlerList handlerList; private final Server server; private final AtomicBoolean isRunning = new AtomicBoolean(false); private final String localhost; public HttpdForTests() throws SocketException { // Configure the logging for jetty. Which uses a singleton. Ho hum. Log.setLog(new JavaUtilLog()); server = new Server(); ServerConnector connector = new ServerConnector(server); connector.addConnectionFactory(new HttpConnectionFactory()); // Choose a port randomly upon listening for socket connections. connector.setPort(0); server.addConnector(connector); handlerList = new HandlerList(); localhost = getLocalhostAddress().getHostAddress(); } public void addHandler(Handler handler) { assertFalse(isRunning.get()); handlerList.addHandler(handler); } public void addStaticContent(String contentToReturn) { addHandler(new StaticContent(contentToReturn)); } public void start() throws Exception { assertTrue(isRunning.compareAndSet(false, true)); handlerList.addHandler(new DefaultHandler()); server.setHandler(handlerList); server.start(); } @Override public void close() throws Exception { server.stop(); server.join(); isRunning.set(false); } public URI getRootUri() { try { return getUri("/"); } catch (SocketException | URISyntaxException e) { // Should never happen throw new RuntimeException(e); } } public URI getUri(String path) throws URISyntaxException, SocketException { assertTrue( "Server must be running before retrieving a URI, otherwise the resulting URI may " + "not have an appropriate port", isRunning.get()); URI baseUri; try { baseUri = server.getURI(); } catch (Exception e) { // We'd rather catch UnknownHostException, but that's a checked exception that is claimed // never to be thrown. baseUri = new URI("http://localhost/"); } Preconditions.checkNotNull(baseUri, "Unable to determine baseUri"); // It turns out that if we got baseUri from Jetty it may have just Made Stuff Up. To avoid this, // we only use the scheme and port that Jetty returned. return new URI( baseUri.getScheme(), /* user info */ null, localhost, baseUri.getPort(), path, null, null); } /** * @return an address that's either on a loopback or local network interface that refers to * localhost. */ private InetAddress getLocalhostAddress() throws SocketException { // It turns out that: // InetAddress.getLocalHost().getHostAddress() // will occasionally just make up return values. I have no idea why. To work around this, figure // out this stuff automagically. // First, we collect every possible InetAddress we might be able to return Set<InetAddress> candidateLoopbacks = new HashSet<>(); Set<InetAddress> candidateLocal = new HashSet<>(); Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); Preconditions.checkNotNull(interfaces, "Apprently this machine has no network interfaces."); while (interfaces.hasMoreElements()) { NetworkInterface iface = interfaces.nextElement(); if (!iface.isUp()) { continue; } if (iface.isLoopback()) { candidateLoopbacks.addAll(getInetAddresses(iface)); } else { candidateLocal.addAll(getInetAddresses(iface)); } } // We need at least one inet address in order to continue. Preconditions.checkState(!candidateLoopbacks.isEmpty() || !candidateLocal.isEmpty()); // Prefer a loopback device to going out over the NIC. if (!candidateLoopbacks.isEmpty()) { return candidateLoopbacks.iterator().next(); } return candidateLocal.iterator().next(); } private Set<InetAddress> getInetAddresses(NetworkInterface iface) { Set<InetAddress> toReturn = new HashSet<>(); Enumeration<InetAddress> addresses = iface.getInetAddresses(); while (addresses.hasMoreElements()) { toReturn.add(addresses.nextElement()); } return toReturn; } private static class StaticContent extends AbstractHandler { private final String content; public StaticContent(String content) { this.content = content; } @Override public void handle( String target, Request request, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException, ServletException { // Use an unusual charset. httpServletResponse.getOutputStream().write(content.getBytes(UTF_16)); request.setHandled(true); } } public static class DummyPutRequestsHandler extends AbstractHandler { private final List<String> putRequestsPaths = new ArrayList<>(); @Override public void handle( String target, Request request, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException, ServletException { if (!HttpMethod.PUT.is(request.getMethod())) { return; } putRequestsPaths.add(request.getUri().toString()); request.setHandled(true); } public List<String> getPutRequestsPaths() { return putRequestsPaths; } } public static class FileDispenserRequestHandler extends ContextHandler { public FileDispenserRequestHandler(Path rootDir) { this(rootDir, "/"); } public FileDispenserRequestHandler(Path rootDir, String urlBasePath) { super(urlBasePath); ResourceHandler resourceHandler = new ResourceHandler(); resourceHandler.setDirectoriesListed(true); resourceHandler.setResourceBase(rootDir.toAbsolutePath().toString()); setHandler(resourceHandler); setLogger(new StdErrLog()); } } }