io.fusionauth.http.server.internal.HTTPServerThread Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of java-http Show documentation
Show all versions of java-http Show documentation
An HTTP library for Java that provides a lightweight server (currently) and client (eventually) both with a goal of high-performance and simplicity
The newest version!
/*
* Copyright (c) 2022-2024, FusionAuth, 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.
* 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 io.fusionauth.http.server.internal;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.security.GeneralSecurityException;
import java.util.Deque;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedDeque;
import io.fusionauth.http.log.Logger;
import io.fusionauth.http.security.SecurityTools;
import io.fusionauth.http.server.HTTPListenerConfiguration;
import io.fusionauth.http.server.HTTPServerConfiguration;
import io.fusionauth.http.server.Instrumenter;
import io.fusionauth.http.server.io.Throughput;
/**
* A thread that manages the accept process for a single server socket. Once a connection is accepted, the socket is passed to a virtual
* thread for processing.
*
* @author Brian Pontarelli
*/
public class HTTPServerThread extends Thread {
private final HTTPServerCleanerThread cleaner;
private final Deque clients = new ConcurrentLinkedDeque<>();
private final HTTPServerConfiguration configuration;
private final Instrumenter instrumenter;
private final HTTPListenerConfiguration listener;
private final Logger logger;
private final long minimumReadThroughput;
private final long minimumWriteThroughput;
private final ServerSocket socket;
private volatile boolean running;
public HTTPServerThread(HTTPServerConfiguration configuration, HTTPListenerConfiguration listener)
throws IOException, GeneralSecurityException {
super("HTTP server [" + listener.getBindAddress().toString() + ":" + listener.getPort() + "]");
this.configuration = configuration;
this.listener = listener;
this.instrumenter = configuration.getInstrumenter();
this.logger = configuration.getLoggerFactory().getLogger(HTTPServerThread.class);
this.minimumReadThroughput = configuration.getMinimumReadThroughput();
this.minimumWriteThroughput = configuration.getMinimumWriteThroughput();
this.cleaner = new HTTPServerCleanerThread();
if (listener.isTLS()) {
SSLContext context = SecurityTools.serverContext(listener.getCertificateChain(), listener.getPrivateKey());
this.socket = context.getServerSocketFactory().createServerSocket();
} else {
this.socket = new ServerSocket();
}
socket.setSoTimeout(0); // Always block
socket.bind(new InetSocketAddress(listener.getBindAddress(), listener.getPort()));
if (instrumenter != null) {
instrumenter.serverStarted();
}
}
@Override
public void run() {
running = true;
cleaner.start();
while (running) {
try {
Socket clientSocket = socket.accept();
clientSocket.setSoTimeout((int) configuration.getInitialReadTimeoutDuration().toMillis());
logger.debug("Accepted inbound connection with [{}] existing connections and initial read timeout of [{}]", clients.size(),
configuration.getInitialReadTimeoutDuration().toMillis());
if (instrumenter != null) {
instrumenter.acceptedConnection();
}
Throughput throughput = new Throughput(configuration.getReadThroughputCalculationDelay().toMillis(), configuration.getWriteThroughputCalculationDelay().toMillis());
HTTPWorker runnable = new HTTPWorker(clientSocket, configuration, instrumenter, listener, throughput);
Thread client = Thread.ofVirtual()
.name("HTTP client [" + clientSocket.getRemoteSocketAddress() + "]")
.start(runnable);
clients.add(new ClientInfo(client, runnable, throughput));
} catch (SocketTimeoutException ignore) {
// Completely smother since this is expected with the SO_TIMEOUT setting in the constructor
logger.debug("Nothing accepted. Cleaning up existing connections.");
} catch (SocketException e) {
// This should only happen when the server is shutdown
if (socket.isClosed()) {
running = false;
logger.debug("The server socket was closed. Shutting down the server.", e);
} else {
logger.error("An exception was thrown while accepting incoming connections.", e);
}
} catch (IOException ignore) {
// Completely smother since most IO exceptions are common during the connection phase
logger.debug("IO exception. Likely a fuzzer or a bad client or a TLS issue, all of which are common and can mostly be ignored.");
} catch (Throwable t) {
logger.error("An exception was thrown during server processing. This is a fatal issue and we need to shutdown the server.", t);
break;
}
}
// Close all the client connections as cleanly as possible
for (ClientInfo client : clients) {
client.thread().interrupt();
}
}
public void shutdown() {
running = false;
try {
cleaner.interrupt();
socket.close();
} catch (IOException ignore) {
// Ignorable since we are shutting down regardless
}
}
record ClientInfo(Thread thread, HTTPWorker runnable, Throughput throughput) {
}
private class HTTPServerCleanerThread extends Thread {
public HTTPServerCleanerThread() {
super("Cleaner for HTTP server [" + listener.getBindAddress().toString() + ":" + listener.getPort() + "]");
}
public void run() {
while (running) {
logger.debug("Cleaning things up");
Iterator iterator = clients.iterator();
while (iterator.hasNext()) {
ClientInfo client = iterator.next();
Thread thread = client.thread();
if (!thread.isAlive()) {
logger.debug("Thread is dead. Removing.");
iterator.remove();
continue;
}
long now = System.currentTimeMillis();
Throughput throughput = client.throughput();
HTTPWorker worker = client.runnable();
HTTPWorker.State state = worker.state();
long workerLastUsed = throughput.lastUsed();
boolean readingSlow = false;
boolean writingSlow = false;
boolean timedOut = false;
boolean badClient = false;
if (state == HTTPWorker.State.Read) {
// Here the SO_TIMEOUT set above or the Keep-Alive timeout in HTTPWorker will dictate if the socket has timed out. This prevents slow readers
// or network issues where the client reads 1 byte per timeout value (i.e. 1 byte per 2 seconds or something like that)
badClient = throughput.readThroughput(now) < minimumReadThroughput;
readingSlow = badClient;
} else if (state == HTTPWorker.State.Write) {
// Check for slow clients when writing (or network issues)
badClient = throughput.writeThroughput(now) < minimumWriteThroughput;
writingSlow = badClient;
} else if (state == HTTPWorker.State.Process) {
// Here lastUsed was the instant the last byte was read, so we calculate distance between that and now to see if it is beyond the timeout
badClient = now - workerLastUsed > configuration.getProcessingTimeoutDuration().toMillis();
timedOut = badClient;
}
logger.debug("Checking client readingSlow=[{}] writingSlow=[{}] timedOut=[{}] writeThroughput=[{}] minWriteThroughput=[{}]", readingSlow, writingSlow, timedOut, throughput.writeThroughput(now), minimumWriteThroughput);
if (!badClient) {
continue;
}
// Bad client, first things first, remove the client from the list
iterator.remove();
if (logger.isDebugEnabled()) {
String message = "";
if (readingSlow) {
message += String.format(" Min read throughput [%s], actual throughput [%s].", minimumReadThroughput, throughput.readThroughput(now));
}
if (writingSlow) {
message += String.format(" Min write throughput [%s], actual throughput [%s].", minimumWriteThroughput, throughput.writeThroughput(now));
}
if (timedOut) {
message += String.format(" Connection timed out while processing. Last used [%s]ms ago. Configured client timeout [%s]ms.", (now - workerLastUsed), configuration.getProcessingTimeoutDuration().toMillis());
}
logger.debug("Closing connection readingSlow=[{}] writingSlow=[{}] timedOut=[{}] {}", readingSlow, writingSlow, timedOut, message);
logger.debug("Closing client connection [{}] due to inactivity", worker.getSocket().getRemoteSocketAddress());
StringBuilder threadDump = new StringBuilder();
for (Map.Entry entry : Thread.getAllStackTraces().entrySet()) {
threadDump.append(entry.getKey()).append(" ").append(entry.getKey().getState()).append("\n");
for (StackTraceElement ste : entry.getValue()) {
threadDump.append("\tat ").append(ste).append("\n");
}
threadDump.append("\n");
}
logger.debug("Thread dump from server side.\n" + threadDump);
}
try {
var socket = worker.getSocket();
socket.close();
if (instrumenter != null) {
instrumenter.connectionClosed();
}
} catch (IOException e) {
// Log but ignore
logger.debug("Unable to close connection to client. [{}]", e);
}
}
// Take a break
try {
//noinspection BusyWait
sleep(2_000);
} catch (InterruptedException ignore) {
// Ignore
}
}
}
}
}