|
| 1 | +/* |
| 2 | + * Copyright (C) 2015 Red Hat, Inc. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | +package io.fabric8.mockwebserver; |
| 17 | + |
| 18 | +import io.fabric8.mockwebserver.http.Dispatcher; |
| 19 | +import io.fabric8.mockwebserver.http.HttpUrl; |
| 20 | +import io.fabric8.mockwebserver.http.MockResponse; |
| 21 | +import io.fabric8.mockwebserver.http.QueueDispatcher; |
| 22 | +import io.fabric8.mockwebserver.http.RecordedHttpConnection; |
| 23 | +import io.fabric8.mockwebserver.http.RecordedRequest; |
| 24 | +import io.fabric8.mockwebserver.vertx.HttpServerRequestHandler; |
| 25 | +import io.fabric8.mockwebserver.vertx.Protocol; |
| 26 | +import io.netty.handler.ssl.ClientAuth; |
| 27 | +import io.vertx.core.AsyncResult; |
| 28 | +import io.vertx.core.Future; |
| 29 | +import io.vertx.core.Handler; |
| 30 | +import io.vertx.core.Vertx; |
| 31 | +import io.vertx.core.http.HttpServer; |
| 32 | +import io.vertx.core.http.HttpServerOptions; |
| 33 | +import io.vertx.core.net.NetServerOptions; |
| 34 | +import io.vertx.core.net.SelfSignedCertificate; |
| 35 | + |
| 36 | +import java.io.Closeable; |
| 37 | +import java.io.IOException; |
| 38 | +import java.net.InetAddress; |
| 39 | +import java.net.InetSocketAddress; |
| 40 | +import java.net.Proxy; |
| 41 | +import java.util.ArrayList; |
| 42 | +import java.util.Arrays; |
| 43 | +import java.util.HashSet; |
| 44 | +import java.util.List; |
| 45 | +import java.util.concurrent.BlockingQueue; |
| 46 | +import java.util.concurrent.CompletableFuture; |
| 47 | +import java.util.concurrent.ExecutionException; |
| 48 | +import java.util.concurrent.LinkedBlockingQueue; |
| 49 | +import java.util.concurrent.TimeUnit; |
| 50 | +import java.util.concurrent.TimeoutException; |
| 51 | +import java.util.concurrent.atomic.AtomicInteger; |
| 52 | +import java.util.logging.Level; |
| 53 | +import java.util.logging.Logger; |
| 54 | +import java.util.stream.Collectors; |
| 55 | + |
| 56 | +import static io.vertx.core.net.SSLOptions.DEFAULT_ENABLED_SECURE_TRANSPORT_PROTOCOLS; |
| 57 | + |
| 58 | +public class MockWebServer implements Closeable { |
| 59 | + |
| 60 | + private static final String[] SUPPORTED_WEBSOCKET_SUB_PROTOCOLS = new String[] { |
| 61 | + "v1.channel.k8s.io", "v2.channel.k8s.io", "v3.channel.k8s.io", "v4.channel.k8s.io" |
| 62 | + }; |
| 63 | + |
| 64 | + private static final Logger logger = Logger.getLogger(MockWebServer.class.getName()); |
| 65 | + |
| 66 | + private final Vertx vertx; |
| 67 | + private final BlockingQueue<RecordedRequest> requestQueue; |
| 68 | + private final AtomicInteger requestCount; |
| 69 | + private final List<MockWebServerListener> listeners; |
| 70 | + private Dispatcher dispatcher; |
| 71 | + private ClientAuth clientAuth; |
| 72 | + private final List<String> enabledSecuredTransportProtocols; |
| 73 | + private boolean ssl; |
| 74 | + private SelfSignedCertificate selfSignedCertificate; |
| 75 | + private HttpServer httpServer; |
| 76 | + private int port; |
| 77 | + private InetAddress inetAddress; |
| 78 | + private String hostName; |
| 79 | + private List<Protocol> protocols; |
| 80 | + private boolean started; |
| 81 | + |
| 82 | + public MockWebServer() { |
| 83 | + vertx = Vertx.vertx(); |
| 84 | + requestQueue = new LinkedBlockingQueue<>(); |
| 85 | + requestCount = new AtomicInteger(); |
| 86 | + listeners = new ArrayList<>(); |
| 87 | + dispatcher = new QueueDispatcher(); |
| 88 | + clientAuth = ClientAuth.NONE; |
| 89 | + enabledSecuredTransportProtocols = new ArrayList<>(); |
| 90 | + enabledSecuredTransportProtocols.addAll(DEFAULT_ENABLED_SECURE_TRANSPORT_PROTOCOLS); |
| 91 | + protocols = Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1); |
| 92 | + } |
| 93 | + |
| 94 | + private void before() { |
| 95 | + if (started) { |
| 96 | + return; |
| 97 | + } |
| 98 | + start(); |
| 99 | + } |
| 100 | + |
| 101 | + public void start() { |
| 102 | + start(NetServerOptions.DEFAULT_PORT); |
| 103 | + } |
| 104 | + |
| 105 | + public void start(int port) { |
| 106 | + start(InetAddress.getLoopbackAddress(), port); |
| 107 | + } |
| 108 | + |
| 109 | + public synchronized void start(InetAddress inetAddress, int port) { |
| 110 | + if (started) { |
| 111 | + throw new IllegalStateException("start() already called"); |
| 112 | + } |
| 113 | + this.started = true; |
| 114 | + this.inetAddress = inetAddress; |
| 115 | + this.hostName = inetAddress.getHostName().equals("127.0.0.1") ? "localhost" : inetAddress.getHostName(); |
| 116 | + final HttpServerOptions options = new HttpServerOptions() |
| 117 | + .setHost(inetAddress.getHostAddress()) |
| 118 | + .setPort(port) |
| 119 | + .setAlpnVersions(protocols.stream().map(Protocol::getHttpVersion).collect(Collectors.toList())) |
| 120 | + .setWebSocketSubProtocols(Arrays.asList(SUPPORTED_WEBSOCKET_SUB_PROTOCOLS)) |
| 121 | + .setHandle100ContinueAutomatically(true); |
| 122 | + if (ssl) { |
| 123 | + selfSignedCertificate = SelfSignedCertificate.create(getHostName()); |
| 124 | + options |
| 125 | + .setSsl(true) |
| 126 | + .setEnabledSecureTransportProtocols(new HashSet<>(enabledSecuredTransportProtocols)) |
| 127 | + .setTrustOptions(selfSignedCertificate.trustOptions()) |
| 128 | + .setKeyCertOptions(selfSignedCertificate.keyCertOptions()); |
| 129 | + } |
| 130 | + httpServer = vertx.createHttpServer(options); |
| 131 | + httpServer.connectionHandler(event -> { |
| 132 | + final RecordedHttpConnection connection = new RecordedHttpConnection( |
| 133 | + event.remoteAddress(), event.localAddress(), ssl); |
| 134 | + listeners.forEach(listener -> listener.onConnection(connection)); |
| 135 | + event.closeHandler(res -> listeners.forEach(listener -> listener.onConnectionClosed(connection))); |
| 136 | + }); |
| 137 | + httpServer.requestHandler(new HttpServerRequestHandler(vertx) { |
| 138 | + @Override |
| 139 | + protected MockResponse onHttpRequest(RecordedRequest request) { |
| 140 | + requestCount.incrementAndGet(); |
| 141 | + requestQueue.add(request); |
| 142 | + final MockResponse response = dispatcher.dispatch(request); |
| 143 | + info("received request: %s and responded: %s", request.toString(), response.toString()); |
| 144 | + return response; |
| 145 | + } |
| 146 | + }); |
| 147 | + await(httpServer.listen(), "Unable to start MockWebServer"); |
| 148 | + this.port = httpServer.actualPort(); |
| 149 | + info("starting to accept connections on %s", getHostName()); |
| 150 | + } |
| 151 | + |
| 152 | + public synchronized void shutdown() { |
| 153 | + if (!started) { |
| 154 | + return; |
| 155 | + } |
| 156 | + if (httpServer == null) { |
| 157 | + throw new IllegalStateException("shutdown() before start()"); |
| 158 | + } |
| 159 | + dispatcher.shutdown(); |
| 160 | + final Future<Void> httpClose = httpServer.close(); |
| 161 | + Handler<AsyncResult<Void>> onComplete = v -> { |
| 162 | + vertx.close(); |
| 163 | + info("done accepting connections"); |
| 164 | + }; |
| 165 | + if (httpClose.isComplete()) { |
| 166 | + onComplete.handle(httpClose); |
| 167 | + } else { |
| 168 | + httpClose.onComplete(onComplete); |
| 169 | + await(httpClose, "Unable to close MockWebServer"); |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + @Override |
| 174 | + public void close() throws IOException { |
| 175 | + shutdown(); |
| 176 | + } |
| 177 | + |
| 178 | + public int getPort() { |
| 179 | + before(); |
| 180 | + return port; |
| 181 | + } |
| 182 | + |
| 183 | + public String getHostName() { |
| 184 | + before(); |
| 185 | + return hostName; |
| 186 | + } |
| 187 | + |
| 188 | + public Proxy toProxyAddress() { |
| 189 | + before(); |
| 190 | + final InetSocketAddress address = new InetSocketAddress(getHostName(), getPort()); |
| 191 | + return new Proxy(Proxy.Type.HTTP, address); |
| 192 | + } |
| 193 | + |
| 194 | + public SelfSignedCertificate getSelfSignedCertificate() { |
| 195 | + return selfSignedCertificate; |
| 196 | + } |
| 197 | + |
| 198 | + public HttpUrl url(String path) { |
| 199 | + if (path.startsWith("/")) { |
| 200 | + path = path.substring(1); |
| 201 | + } |
| 202 | + final String schema = ssl ? "https" : "http"; |
| 203 | + return HttpUrl.parse(schema + "://" + getHostName() + ":" + getPort() + "/" + path); |
| 204 | + } |
| 205 | + |
| 206 | + public RecordedRequest takeRequest() throws InterruptedException { |
| 207 | + return requestQueue.take(); |
| 208 | + } |
| 209 | + |
| 210 | + public RecordedRequest takeRequest(long timeout, TimeUnit unit) throws InterruptedException { |
| 211 | + return requestQueue.poll(timeout, unit); |
| 212 | + } |
| 213 | + |
| 214 | + public int getRequestCount() { |
| 215 | + return requestCount.get(); |
| 216 | + } |
| 217 | + |
| 218 | + public void useHttps() { |
| 219 | + this.ssl = true; |
| 220 | + } |
| 221 | + |
| 222 | + public void enqueue(MockResponse response) { |
| 223 | + if (dispatcher instanceof QueueDispatcher) { |
| 224 | + ((QueueDispatcher) dispatcher).enqueueResponse(response); |
| 225 | + } else { |
| 226 | + throw new IllegalStateException("Dispatcher is not a QueueDispatcher"); |
| 227 | + } |
| 228 | + } |
| 229 | + |
| 230 | + public void addListener(MockWebServerListener listener) { |
| 231 | + listeners.add(listener); |
| 232 | + } |
| 233 | + |
| 234 | + public void setDispatcher(Dispatcher dispatcher) { |
| 235 | + this.dispatcher = dispatcher; |
| 236 | + } |
| 237 | + |
| 238 | + public void setProtocols(List<Protocol> protocols) { |
| 239 | + this.protocols = protocols; |
| 240 | + } |
| 241 | + |
| 242 | + private static <T> T await(Future<T> vertxFuture, String errorMessage) { |
| 243 | + final CompletableFuture<T> future = new CompletableFuture<>(); |
| 244 | + vertxFuture.onComplete(r -> { |
| 245 | + if (r.succeeded()) { |
| 246 | + future.complete(r.result()); |
| 247 | + } else { |
| 248 | + future.completeExceptionally(r.cause()); |
| 249 | + } |
| 250 | + }); |
| 251 | + try { |
| 252 | + return future.get(10, TimeUnit.SECONDS); |
| 253 | + } catch (InterruptedException e) { |
| 254 | + Thread.currentThread().interrupt(); |
| 255 | + throw new IllegalStateException(e); |
| 256 | + } catch (ExecutionException | TimeoutException e) { |
| 257 | + throw new IllegalStateException(errorMessage, e); |
| 258 | + } |
| 259 | + } |
| 260 | + |
| 261 | + private void info(String log, String... parameters) { |
| 262 | + if (logger.isLoggable(Level.INFO)) { |
| 263 | + final String formatMessage = "%s " + log; |
| 264 | + final String[] allParams = Arrays.copyOf(new String[] { toString() }, parameters.length + 1); |
| 265 | + System.arraycopy(parameters, 0, allParams, 1, parameters.length); |
| 266 | + logger.info(String.format(formatMessage, allParams)); |
| 267 | + } |
| 268 | + } |
| 269 | + |
| 270 | + @Override |
| 271 | + public String toString() { |
| 272 | + return "MockWebServer[" + getPort() + "]"; |
| 273 | + } |
| 274 | +} |
0 commit comments