SocketServer.java

/*
 * Copyright (c) 2025 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0, which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package org.glassfish.jersey.examples.expect100continue.netty.connector;

import javax.net.ServerSocketFactory;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

public class SocketServer {
    private static final String NO_CONTENT_HEADER = "HTTP/1.1 204 No Content";
    private static final String OK_HEADER = "HTTP/1.1 200 OK";
    private static final String EXPECT_HEADER = "HTTP/1.1 100 Continue";
    private final ExecutorService executorService = Executors.newCachedThreadPool();
    private AtomicBoolean expect_processed = new AtomicBoolean(false);

    private ServerSocket server;

    private static final boolean debug = true;

    private static final int port = 3000;

    private volatile boolean stopped = false;

    public static void main(String args[]) throws IOException {
        new SocketServer(port).runServer();
    }

    SocketServer(int port) throws IOException {
        final ServerSocketFactory socketFactory = ServerSocketFactory.getDefault();
        server = socketFactory.createServerSocket(port);
    }

    void stop() {
        stopped = true;
        try {
            server.close();
            executorService.shutdown();
            while (!executorService.isTerminated()) {
                executorService.awaitTermination(100, TimeUnit.MILLISECONDS);
            }
        } catch (IOException | InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    void runServer() {

        executorService.execute(() -> {
            try {
                dumpServerReadMe();
                while (!stopped) {
                    final Socket socket = server.accept();
                    executorService.submit(() -> processRequest(socket));
                }
            } catch (IOException e) {
                if (!stopped) {
                    e.printStackTrace();
                }
            }
        });
    }

    private void processRequest(final Socket request) {

        try (final BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
             final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(request.getOutputStream()))) {


            while (!stopped) {
                final Map<String, String> headers = mapHeaders(reader);

                if (headers.isEmpty()) {
                    continue;
                }
                if (debug) {
                    dumpHeaders(headers);
                }

                boolean failed = processExpect100Continue(headers, writer);

                if (failed) {
                    continue;
                }

                final String http_header = expect_processed.get() ? NO_CONTENT_HEADER : OK_HEADER;
                boolean read = readBody(reader, headers);

                final StringBuffer responseBuffer = new StringBuffer(http_header);
                addNewLineToResponse(responseBuffer);
                addServerHeaderToResponse(responseBuffer);
//                    addToResponse("Content-Length: 0", responseBuffer);
                addNewLineToResponse(responseBuffer);
                addNewLineToResponse(responseBuffer);
                if (debug) {
                    dumpResponse(responseBuffer);
                }

                writer.write(responseBuffer.toString());

                writer.flush();
                if (read) {
                    break;
                }

            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                request.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void addNewLineToResponse(StringBuffer responseBuffer) {
        addToResponse("\r\n", responseBuffer);
    }

    private void addToResponse(String toBeAdded, StringBuffer responseBuffer) {
        responseBuffer.append(toBeAdded);
    }

    private void addServerHeaderToResponse(StringBuffer responseBuffer) {
        addToResponse("Server: Example Socket Server v.0.0.1", responseBuffer);
        addNewLineToResponse(responseBuffer);
    }

    private boolean processExpect100Continue(Map<String, String> headers, BufferedWriter writer) throws IOException {
        String http_header = EXPECT_HEADER;
        boolean failed = false;
        final String continueHeader = headers.remove("expect");

        if (continueHeader != null && continueHeader.contains("100-continue")) {

            expect_processed.set(http_header.equals(EXPECT_HEADER));


            final StringBuffer responseBuffer = new StringBuffer(http_header);

            addNewLineToResponse(responseBuffer);
            addToResponse("Connection: keep-alive", responseBuffer);
            addNewLineToResponse(responseBuffer);
            addNewLineToResponse(responseBuffer);
            if (debug) {
                dumpResponse(responseBuffer);
            }

            writer.write(responseBuffer.toString());
            writer.flush();
        }
        return failed;
    }

    private Map<String, String> mapHeaders(BufferedReader reader) throws IOException {
        String line;
        final Map<String, String> headers = new HashMap<>();


        if (!reader.ready()) {
            return headers;
        }

        while ((line = reader.readLine()) != null && !line.isEmpty()) {


            int pos = line.indexOf(':');
            if (pos > -1) {
                headers.put(
                        line.substring(0, pos).toLowerCase(Locale.ROOT),
                        line.substring(pos + 2).toLowerCase(Locale.ROOT).trim());
            }
        }

        return headers;
    }

    private boolean readBody(BufferedReader reader, Map<String, String> headers) throws IOException {
        if (headers.containsKey("content-length")) {
            int contentLength = Integer.valueOf(headers.get("content-length"));
            int actualLength = 0, readingByte = 0;
            int[] buffer = new int[contentLength];
            while (actualLength < contentLength && (readingByte = reader.read()) != -1) {
                buffer[actualLength++] = readingByte;
            }
            if (debug) {
                System.out.println("Reading " + actualLength + " of " + contentLength + " bytes/chars");
            }
            return (actualLength == contentLength);
        } else if (headers.containsKey("transfer-encoding")) {
            String line;
            while ((line = reader.readLine()) != null && !line.equals("0")) {
                if (debug) {
                    System.out.println(line);
                }
            }
            return true;
        }
        return false;
    }

    private void dumpHeaders(Map<String, String> headers) {
        System.out.println("==== DUMPING HEADERS ====");
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            System.out.println(entry.getKey() + ", " + entry.getValue());
        }
        System.out.println("==== HEADERS DUMPED =====");
    }

    private void dumpResponse(StringBuffer responseBuffer) {
        System.out.println("==== DUMPING RESPONSE ====");
        System.out.println(responseBuffer);
        System.out.println("==== RESPONSE DUMPED =====");
    }

    private void dumpServerReadMe() {
        System.out.println("==================================Server is running========================================");
        System.out.println("=                                       ***                                               =");
        System.out.println("= ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ =");
        System.out.println("=  You can send requests to it either using Netty Client or curl or any other http tool.  =");
        System.out.println("=  Try to modify it to see how Expect: 100-continue header works.                         =");
        System.out.println("= ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ =");
        System.out.println("=                               stop server by Ctrl-c                                     =");
        System.out.println("= ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ =");
        System.out.println("=  Run server using maven:                                                                =");
        System.out.println("=          mvn clean package exec:java                                                    =");
        System.out.println("=  Run client using maven:                                                                =");
        System.out.println("=          mvn clean package exec:java -Pclient                                           =");
        System.out.println("= ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ =");
        System.out.println("=                                       ***                                               =");
        System.out.println("===========================================================================================");
    }

}