package com.xebialabs.restito.server; import com.xebialabs.restito.semantics.Call; import com.xebialabs.restito.semantics.Stub; import com.xebialabs.restito.support.log.CallsHelper; import org.apache.mina.util.AvailablePortFinder; import org.glassfish.grizzly.PortRange; import org.glassfish.grizzly.http.server.*; import org.glassfish.grizzly.http.util.HttpStatus; import org.glassfish.grizzly.ssl.SSLContextConfigurator; import org.glassfish.grizzly.ssl.SSLEngineConfigurator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.List; import java.util.ListIterator; import java.util.concurrent.CopyOnWriteArrayList; import static java.util.Collections.unmodifiableList; /** * The HttpServer wrapper which is responsible for operations like starting and stopping and holding objects that describe server behavior. */ public class StubServer { @SuppressWarnings("WeakerAccess") public final static int DEFAULT_PORT = 6666; private final List<Call> calls = new CopyOnWriteArrayList<>(); private final List<Stub> stubs = new CopyOnWriteArrayList<>(); private final HttpServer simpleServer; /** * Whether or not the server should run in HTTPS mode. */ public boolean secured; private Logger log = LoggerFactory.getLogger(StubServer.class); /** * Creates a server based on stubs that are used to determine behavior. */ public StubServer(Stub... stubs) { this(DEFAULT_PORT, AvailablePortFinder.MAX_PORT_NUMBER, stubs); } /** * This constructor allows to specify the port range beside stubs. * Grizzly will select the first available port. */ public StubServer(int portRangeStart, int portRangeEnd, Stub... stubs) { this.stubs.addAll(Arrays.asList(stubs)); simpleServer = HttpServer.createSimpleServer(null, new PortRange(portRangeStart, portRangeEnd)); } /** * This constructor allows to specify the port beside stubs. * If the port is busy, Restito won't try to pick different one and java.net.BindException will be thrown. */ public StubServer(int port, Stub... stubs) { this.stubs.addAll(Arrays.asList(stubs)); simpleServer = HttpServer.createSimpleServer(null, port); } /** * It is possible to add a stub even after the server is started */ public StubServer addStub(Stub s) { this.stubs.add(s); return this; } /** * Removes any previously registered stubs. */ public StubServer clearStubs() { this.stubs.clear(); return this; } /** * Removes any registered stubs calls. */ public StubServer clearCalls() { this.calls.clear(); return this; } /** * Clears the server state. */ public StubServer clear() { this.clearStubs(); this.clearCalls(); return this; } /** * Starts the server */ public StubServer run() { simpleServer.getServerConfiguration().addHttpHandler(stubsToHandler(), "/"); try { if (secured) { for (NetworkListener networkListener : simpleServer.getListeners()) { networkListener.setSecure(true); SSLEngineConfigurator sslEngineConfig = new SSLEngineConfigurator(getSslConfig(), false, false, false); networkListener.setSSLEngineConfig(sslEngineConfig); } } simpleServer.start(); } catch (Exception e) { throw new RuntimeException(e); } return this; } private SSLContextConfigurator getSslConfig() throws IOException { if(SSLContextConfigurator.DEFAULT_CONFIG.validateConfiguration(true)) { return SSLContextConfigurator.DEFAULT_CONFIG; } SSLContextConfigurator defaultConfig = SSLContextConfigurator.DEFAULT_CONFIG; String keystore_server = createCertificateStore("keystore_server"); String truststore_server = createCertificateStore("truststore_server"); defaultConfig.setKeyStoreFile(keystore_server); defaultConfig.setKeyStorePass("secret"); defaultConfig.setTrustStoreFile(truststore_server); defaultConfig.setTrustStorePass("secret"); return defaultConfig; } /** * Copy the Certificate store to the temporary directory, as it needs to be in a real file, not inside a jar for Grizzly to pick it up. * @param resourceName The Store to copy * @return The absolute path to the temporary keystore. * @throws IOException If the store could not be copied. */ private String createCertificateStore(String resourceName) throws IOException { URL resource = StubServer.class.getResource("/" + resourceName); File store = File.createTempFile(resourceName, "store"); try (InputStream input = resource.openStream()) { Files.copy(input, store.toPath(), StandardCopyOption.REPLACE_EXISTING); } finally { store.deleteOnExit(); } return store.getAbsolutePath(); } /** * Alias for StubServer.run() */ public void start() { run(); } /** * Stops the server */ public StubServer stop() { simpleServer.shutdownNow(); return this; } /** * Sets the Server in Secure mode. If it is already running, ignores the call. */ public StubServer secured() { if (!simpleServer.isStarted()) { this.secured = true; } return this; } /** * Returns the port which the server is running at */ public int getPort() { return simpleServer.getListeners().iterator().next().getPort(); } /** * Returns calls performed to the server. Returned list is actually a copy of the original one. This is done to prevent concurrency issues. See <a href="https://github.com/mkotsur/restito/issues/33">#33</a>. */ public List<Call> getCalls() { return unmodifiableList(calls); } /** * Returns stubs associated with the server. Returned list is actually a copy of the original one. This is done to prevent concurrency issues. See <a href="https://github.com/mkotsur/restito/issues/33">#33</a>. */ public List<Stub> getStubs() { return unmodifiableList(stubs); } private HttpHandler stubsToHandler() { return new HttpHandler() { @Override public void service(Request request, Response response) throws Exception { Call call = Call.fromRequest(request); CallsHelper.logCall(call); boolean processed = false; ListIterator<Stub> iterator = stubs.listIterator(stubs.size()); while (iterator.hasPrevious()) { Stub stub = iterator.previous(); if (!stub.isApplicable(call)) { continue; } stub.apply(response); processed = true; break; } if (!processed) { response.setStatus(HttpStatus.NOT_FOUND_404); log.warn("Request {} hasn't been covered by any of {} stubs.", request.getRequestURI(), stubs.size()); } calls.add(call); } }; } }