All Downloads are FREE. Search and download functionalities are using the official Maven repository.

software.amazon.awssdk.http.server.MockServer Maven / Gradle / Ivy

/*
 * Copyright Amazon.com, Inc. or its affiliates. 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.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http.server;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLServerSocketFactory;
import software.amazon.awssdk.http.SdkHttpFullRequest;
import software.amazon.awssdk.http.TlsKeyManagersProvider;
import software.amazon.awssdk.utils.Logger;

/**
 * MockServer implementation with several different configurable behaviors.
 */
public class MockServer {

    private static final Logger logger = Logger.loggerFor(MockServer.class);
    private final ServerBehaviorStrategy serverBehaviorStrategy;
    private ServerSocket serverSocket;
    private Thread listenerThread;

    public MockServer(ServerBehaviorStrategy serverBehaviorStrategy) {
        this.serverBehaviorStrategy = serverBehaviorStrategy;
    }

    public static MockServer createMockServer(ServerBehavior serverBehavior) {
        switch (serverBehavior) {
            case HALF_CLOSE:
                if (!isTlsHalfCloseSupported()) {
                    throw new UnsupportedOperationException("Half close is not supported in the current Java Version "
                                                            + getJavaVersion());
                }
                return new MockServer(new HalfCloseServerBehavior());
            case FULL_CLOSE_IN_BETWEEN:
                return new MockServer(new FullCloseInBetweenServerBehavior());
            case FULL_CLOSE_AT_THE_END:
                return new MockServer(new FullCloseAtTheEndServerBehavior());
            default:
                throw new IllegalArgumentException("Unsupported implementation for server issue: " + serverBehavior);
        }
    }

    private static Map parseHeaders(String headers) {
        Map headerMap = new HashMap<>();
        String[] lines = headers.split("\r\n");
        for (String line : lines) {
            if (line.contains(":")) {
                String[] parts = line.split(":", 2);
                headerMap.put(parts[0].trim(), parts[1].trim());
            }
        }
        return headerMap;
    }

    private static String toByteToString(ByteArrayOutputStream baos) {
        return StandardCharsets.UTF_8.decode(ByteBuffer.wrap(baos.toByteArray())).toString();
    }

    public void startServer(TlsKeyManagersProvider keyManagersProvider) {
        try {
            SSLContext ctx = SSLContext.getInstance("SSL");
            ctx.init(keyManagersProvider.keyManagers(), null, null);
            SSLServerSocketFactory ssf = ctx.getServerSocketFactory();
            serverSocket = ssf.createServerSocket(0);
            logger.info(() -> "Listening on port " + serverSocket.getLocalPort());
        } catch (Exception e) {
            throw new RuntimeException("Unable to start the server socket.", e);
        }
        listenerThread = new MockServerListenerThread(serverSocket, serverBehaviorStrategy);
        listenerThread.setDaemon(true);
        listenerThread.start();
    }

    public void stopServer() {
        listenerThread.interrupt();
        try {
            listenerThread.join(5 * 1000);
        } catch (InterruptedException e1) {
            logger.error(() -> "The listener thread didn't terminate after waiting for 10 seconds.");
        }
        if (serverSocket != null) {
            try {
                serverSocket.close();
            } catch (IOException e) {
                throw new RuntimeException("Unable to stop the server socket.", e);
            }
        }
    }

    public int getPort() {
        return serverSocket.getLocalPort();
    }

    public SdkHttpFullRequest.Builder configureHttpsEndpoint(SdkHttpFullRequest.Builder request) {
        return request.uri(URI.create("https://localhost"))
                      .port(getPort());
    }

    public SdkHttpFullRequest.Builder configureHttpEndpoint(SdkHttpFullRequest.Builder request) {
        return request.uri(URI.create("http://localhost"))
                      .port(getPort());
    }

    public enum ServerBehavior {
        HALF_CLOSE,
        FULL_CLOSE_IN_BETWEEN,
        FULL_CLOSE_AT_THE_END
    }

    public interface ServerBehaviorStrategy {
        void runServer(ServerSocket serverSocket);
    }

    private static class MockServerListenerThread extends Thread {

        private final ServerSocket serverSocket;
        private final ServerBehaviorStrategy behaviorStrategy;

        MockServerListenerThread(ServerSocket serverSocket, ServerBehaviorStrategy behaviorStrategy) {
            super(behaviorStrategy.getClass().getName());
            this.serverSocket = serverSocket;
            this.behaviorStrategy = behaviorStrategy;
            setDaemon(true);
        }

        @Override
        public void run() {
            this.behaviorStrategy.runServer(serverSocket);
        }
    }

    public static class HalfCloseServerBehavior implements ServerBehaviorStrategy {

        @Override
        public void runServer(ServerSocket serverSocket) {
            Socket socket = null;
            try {
                while (true) {

                    try {
                        socket = serverSocket.accept();
                        byte[] buff = new byte[4096];
                        ByteArrayOutputStream headerStream = new ByteArrayOutputStream();
                        int read;
                        while ((read = socket.getInputStream().read(buff)) != -1) {
                            headerStream.write(buff, 0, read);
                            String headers = toByteToString(headerStream);
                            if (headers.contains("\r\n\r\n")) {
                                break;
                            }
                        }
                        Thread.sleep(100);
                        socket.shutdownOutput();
                        try (DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
                            out.writeBytes("HTTP/1.1 200 OK\r\n");
                            out.writeBytes("Content-Type: text/html\r\n");
                            out.writeBytes("Content-Length: 0\r\n\r\n");
                            out.flush();
                        }

                    } catch (SocketException se) {
                        // Socket is already closed so expected
                        return;
                    } catch (InterruptedException e) {
                        return;
                    }
                }
            } catch (IOException e) {
                throw new RuntimeException("Error when waiting for new socket connection.", e);
            } finally {
                closeQuietly(socket);
            }
        }
    }

    public static class FullCloseInBetweenServerBehavior implements ServerBehaviorStrategy {

        @Override
        public void runServer(ServerSocket serverSocket) {
            Socket socket = null;
            try {
                while (true) {

                    try {
                        socket = serverSocket.accept();
                        byte[] buff = new byte[4096];
                        ByteArrayOutputStream headerStream = new ByteArrayOutputStream();
                        int read;
                        while ((read = socket.getInputStream().read(buff)) != -1) {
                            headerStream.write(buff, 0, read);
                            String headers = toByteToString(headerStream);
                            if (headers.contains("\r\n\r\n")) {
                                break;
                            }
                        }
                        socket.close();
                        try (DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
                            out.writeBytes("HTTP/1.1 200 OK\r\n");
                            out.writeBytes("Content-Type: text/html\r\n");
                            out.writeBytes("Content-Length: 0\r\n\r\n");
                            out.flush();
                        }
                    } catch (SocketException se) {
                        // Socket is already closed so expected
                        return;
                    }
                }
            } catch (IOException e) {
                throw new RuntimeException("Error when waiting for new socket connection.", e);
            } finally {
                closeQuietly(socket);
            }
        }
    }

    public static class FullCloseAtTheEndServerBehavior implements ServerBehaviorStrategy {

        @Override
        public void runServer(ServerSocket serverSocket) {
            Socket socket = null;
            try {
                while (true) {

                    try {
                        socket = serverSocket.accept();
                        ByteArrayOutputStream headerStream = new ByteArrayOutputStream();
                        byte[] buff = new byte[4096];
                        int read;
                        while ((read = socket.getInputStream().read(buff)) != -1) {
                            headerStream.write(buff, 0, read);
                            String headers = toByteToString(headerStream);
                            if (headers.contains("\r\n\r\n")) {
                                break;
                            }
                        }
                        String headers = toByteToString(headerStream);
                        Map headerMap = parseHeaders(headers);
                        int contentLength = Integer.parseInt(headerMap.getOrDefault("Content-Length", "0"));
                        if (headers.startsWith("PUT")) {
                            ByteArrayOutputStream bodyStream = new ByteArrayOutputStream();
                            int remaining = contentLength;
                            while (remaining > 0 &&
                                   (read = socket.getInputStream().read(buff, 0, Math.min(buff.length, remaining))) != -1) {
                                bodyStream.write(buff, 0, read);
                                remaining -= read;
                            }
                        }
                        try (DataOutputStream out = new DataOutputStream(socket.getOutputStream())) {
                            out.writeBytes("HTTP/1.1 200 OK\r\n");
                            out.writeBytes("Content-Type: text/html\r\n");
                            out.writeBytes("Content-Length: 0\r\n\r\n");
                            out.flush();
                        }
                        while (true) {
                            // Stop server will interrupt to stop this thread.
                            Thread.sleep(1000);
                        }

                    } catch (SocketException se) {
                        return;
                    } catch (InterruptedException e) {
                        return;
                    }
                }
            } catch (IOException e) {
                throw new RuntimeException("Error when waiting for new socket connection.", e);
            } finally {
                closeQuietly(socket);
            }
        }
    }


    //  TLS1.3 enbaled by default for 8u341 onwards.
    //  https://www.oracle.com/java/technologies/javase/8u341-relnotes.html
    public static boolean isTlsHalfCloseSupported() {
        String javaVersion = getJavaVersion();
        String[] versionComponents = javaVersion.split("_");
        if (versionComponents.length == 2) {
            try {
                int buildNumber = Integer.parseInt(versionComponents[1].split("-")[0]);
                if (javaVersion.startsWith("1.8.0") && buildNumber < 341) {
                    return false;
                }
            } catch (NumberFormatException e) {
                logger.error(() -> "Invalid Java version format: " + javaVersion);
                throw e;
            }
        }
        return true;
    }

    public static void closeQuietly(Socket socket) {
        if (socket != null) {
            try {
                socket.close();
            } catch (Exception ex) {
                logger.debug(() -> "Ignore failure in closing the socket", ex);
            }
        }
    }

    private static String getJavaVersion() {
        // CHECKSTYLE:OFF
        return System.getProperty("java.version");
        // CHECKSTYLE:ON
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy