diff --git a/build.savant b/build.savant
index 59b092a..b03485d 100644
--- a/build.savant
+++ b/build.savant
@@ -62,12 +62,9 @@ release = loadPlugin(id: "org.savantbuild.plugin:release-git:2.0.0-RC.6")
pom = loadPlugin(id: "org.savantbuild.plugin:pom:2.0.0-RC.6")
java.settings.javaVersion = "17"
-java
- .settings
- .compilerArguments = "--add-exports java.base/sun.security.x509=ALL-UNNAMED --add-exports java.base/sun.security.util=ALL-UNNAMED -XDignore.symbol.file"
+java.settings.compilerArguments = "--add-exports java.base/sun.security.x509=ALL-UNNAMED --add-exports java.base/sun.security.util=ALL-UNNAMED -XDignore.symbol.file"
javaTestNG.settings.javaVersion = "17"
-javaTestNG
- .settings.jvmArguments = "--add-exports java.base/sun.security.x509=ALL-UNNAMED --add-exports java.base/sun.security.util=ALL-UNNAMED"
+javaTestNG.settings.jvmArguments = "--add-exports java.base/sun.security.x509=ALL-UNNAMED --add-exports java.base/sun.security.util=ALL-UNNAMED"
javaTestNG.settings.testngArguments = "-listener io.fusionauth.http.BaseTest\$TestListener"
target(name: "clean", description: "Cleans the build directory") {
diff --git a/java-http.ipr b/java-http.ipr
index d5d5ee7..5b486dd 100644
--- a/java-http.ipr
+++ b/java-http.ipr
@@ -1385,6 +1385,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/io/fusionauth/http/log/AccumulatingLogger.java b/src/main/java/io/fusionauth/http/log/AccumulatingLogger.java
index af6a2f3..904b8c6 100644
--- a/src/main/java/io/fusionauth/http/log/AccumulatingLogger.java
+++ b/src/main/java/io/fusionauth/http/log/AccumulatingLogger.java
@@ -27,7 +27,7 @@
public class AccumulatingLogger extends BaseLogger {
private final List messages = new ArrayList<>();
- public void reset() {
+ public synchronized void reset() {
messages.clear();
}
@@ -37,7 +37,7 @@ public String toString() {
}
@Override
- protected void handleMessage(String message) {
+ protected synchronized void handleMessage(String message) {
messages.add(message);
}
}
diff --git a/src/main/java/io/fusionauth/http/server/HTTP11Processor.java b/src/main/java/io/fusionauth/http/server/HTTP11Processor.java
index 5badedd..1da2f2d 100644
--- a/src/main/java/io/fusionauth/http/server/HTTP11Processor.java
+++ b/src/main/java/io/fusionauth/http/server/HTTP11Processor.java
@@ -181,6 +181,7 @@ public ProcessorState read(ByteBuffer buffer) throws IOException {
state = ProcessorState.Write;
}
+ logger.trace("(RR)");
return state;
}
diff --git a/src/main/java/io/fusionauth/http/server/HTTPRequestProcessor.java b/src/main/java/io/fusionauth/http/server/HTTPRequestProcessor.java
index 31450b9..d3165a2 100644
--- a/src/main/java/io/fusionauth/http/server/HTTPRequestProcessor.java
+++ b/src/main/java/io/fusionauth/http/server/HTTPRequestProcessor.java
@@ -66,6 +66,7 @@ public ByteBuffer bodyBuffer() {
public RequestState processBodyBytes() {
bodyProcessor.processBuffer(inputStream);
+ logger.trace("(BODY) {} {}", bodyProcessor.currentBuffer(), bodyProcessor.totalBytesProcessed());
if (bodyProcessor.isComplete()) {
inputStream.signalDone();
@@ -115,7 +116,7 @@ public RequestState processPreambleBytes(ByteBuffer buffer) {
int size = Math.max(buffer.remaining(), bufferSize);
if (contentLength != null) {
- logger.debug("Handling body using Content-Length header");
+ logger.debug("Handling body using Content-Length header {}", contentLength);
bodyProcessor = new ContentLengthBodyProcessor(size, contentLength);
} else {
logger.debug("Handling body using Chunked data");
diff --git a/src/main/java/io/fusionauth/http/server/HTTPResponseProcessor.java b/src/main/java/io/fusionauth/http/server/HTTPResponseProcessor.java
index 03e27fd..2c8288d 100644
--- a/src/main/java/io/fusionauth/http/server/HTTPResponseProcessor.java
+++ b/src/main/java/io/fusionauth/http/server/HTTPResponseProcessor.java
@@ -70,7 +70,7 @@ public synchronized ByteBuffer[] currentBuffer() {
// Construct the preamble if needed and return it if there is any bytes left
if (preambleBuffers == null) {
- logger.debug("The worker thread has bytes to write or has closed the stream, but the preamble hasn't been sent yet. Generating preamble");
+ logger.debug("The server (via a worker thread or the server due to an Expect request) has bytes to write or has closed the stream, but the preamble hasn't been sent yet. Generating preamble");
int maxHeadLength = configuration.getMaxHeadLength();
if (state == ResponseState.Preamble) {
fillInHeaders();
diff --git a/src/main/java/io/fusionauth/http/server/HTTPS11Processor.java b/src/main/java/io/fusionauth/http/server/HTTPS11Processor.java
index 24db1e2..d7509ad 100644
--- a/src/main/java/io/fusionauth/http/server/HTTPS11Processor.java
+++ b/src/main/java/io/fusionauth/http/server/HTTPS11Processor.java
@@ -17,42 +17,41 @@
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLEngineResult.Status;
+import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.security.GeneralSecurityException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicInteger;
+import io.fusionauth.http.ParseException;
import io.fusionauth.http.log.Logger;
import io.fusionauth.http.security.SecurityTools;
public class HTTPS11Processor implements HTTPProcessor {
- private static final AtomicInteger threadCount = new AtomicInteger(1);
-
- private static final ExecutorService executor = Executors.newCachedThreadPool(r -> new Thread(r, "TLS Handshake Thread " + threadCount.getAndIncrement()));
+ private final ByteBuffer[] encryptedDataArray;
private final SSLEngine engine;
- private final Logger logger;
-
- private final ByteBuffer[] myAppData;
+ private final ByteBuffer[] handshakeDataArray;
- private final ByteBuffer[] myNetData;
+ private final Logger logger;
- private final ByteBuffer peerAppData;
+ private ByteBuffer decryptedData;
private HTTP11Processor delegate;
- private volatile ProcessorState handshakeState;
+ private ByteBuffer encryptedData;
- private ByteBuffer peerNetData;
+ private ByteBuffer handshakeData;
- public HTTPS11Processor(HTTP11Processor delegate, HTTPServerConfiguration configuration, HTTPListenerConfiguration listenerConfiguration)
+ private volatile HTTPSState state;
+
+ public HTTPS11Processor(HTTP11Processor delegate, HTTPServerConfiguration configuration,
+ HTTPListenerConfiguration listenerConfiguration)
throws GeneralSecurityException, IOException {
this.delegate = delegate;
this.logger = configuration.getLoggerFactory().getLogger(HTTPS11Processor.class);
@@ -63,58 +62,66 @@ public HTTPS11Processor(HTTP11Processor delegate, HTTPServerConfiguration config
this.engine.setUseClientMode(false);
SSLSession session = engine.getSession();
- this.myAppData = new ByteBuffer[]{ByteBuffer.allocate(session.getApplicationBufferSize())};
- this.myNetData = new ByteBuffer[]{ByteBuffer.allocate(session.getPacketBufferSize())};
- this.peerAppData = ByteBuffer.allocate(session.getApplicationBufferSize());
- this.peerNetData = ByteBuffer.allocate(session.getPacketBufferSize());
-
- // Set the remaining on the myNetData to be 0. This is how we tell the write operation that we have nothing to write, so it can handshake/wrap
- this.myNetData[0].flip();
+ this.decryptedData = ByteBuffer.allocate(session.getApplicationBufferSize());
+ this.encryptedData = ByteBuffer.allocate(session.getPacketBufferSize());
+ this.handshakeData = ByteBuffer.allocate(session.getPacketBufferSize());
+ this.encryptedDataArray = new ByteBuffer[]{encryptedData};
+ this.handshakeDataArray = new ByteBuffer[]{handshakeData};
engine.beginHandshake();
HandshakeStatus tlsStatus = engine.getHandshakeStatus();
if (tlsStatus == HandshakeStatus.NEED_UNWRAP) {
- this.handshakeState = ProcessorState.Read;
+ this.state = HTTPSState.HandshakeRead;
} else if (tlsStatus == HandshakeStatus.NEED_WRAP) {
- this.handshakeState = ProcessorState.Write;
+ this.state = HTTPSState.HandshakeWrite;
} else {
throw new IllegalStateException("The SSLEngine is not in a valid state. It should be in the handshake state, but it is in the state [" + tlsStatus + "]");
}
} else {
this.engine = null;
- this.myAppData = null;
- this.myNetData = null;
- this.peerAppData = null;
- this.peerNetData = null;
+ this.decryptedData = null;
+ this.encryptedData = null;
+ this.encryptedDataArray = null;
+ this.handshakeData = null;
+ this.handshakeDataArray = null;
}
}
@Override
public ProcessorState close(boolean endOfStream) {
- logger.trace("(HTTPS-C)");
-
if (this.engine == null) {
return delegate.close(endOfStream);
}
- if (endOfStream) {
- try {
- engine.closeInbound();
- } catch (IOException e) {
- // Smother
- }
+ logger.trace("(HTTPS-CLOSE) {} {}", engine.isInboundDone(), engine.isOutboundDone());
+
+ engine.getSession().invalidate();
+
+ try {
+ delegate.close(endOfStream);
+ engine.closeOutbound();
+ state = HTTPSState.HandshakeWrite;
+
+ encryptedData.clear();
+ decryptedData.clear();
+ var result = engine.wrap(decryptedData, encryptedData);
+ logger.trace("(HTTPS-CLOSE) {} {} {} {} {} {} {}", engine.isInboundDone(), engine.isOutboundDone(), encryptedData, decryptedData, result.getStatus(), result.getHandshakeStatus(), state);
+ } catch (SSLException e) {
+ // Ignore since we are closing
}
- delegate.close(endOfStream);
- handshakeState = ProcessorState.Write;
- engine.closeOutbound();
- return handshakeState;
+ return toProcessorState();
}
@Override
public void failure(Throwable t) {
- logger.trace("(HTTPS-F)");
+ logger.trace("(HTTPS-FAILURE)");
delegate.failure(t);
+ state = switch (delegate.state()) {
+ case Close -> HTTPSState.Close;
+ case Write -> HTTPSState.BodyWrite;
+ default -> throw new IllegalStateException("Unexpected failure state from the HTTP11Processor (delegate to the HTTPS11Processor)");
+ };
}
/**
@@ -124,12 +131,12 @@ public void failure(Throwable t) {
*/
@Override
public int initialKeyOps() {
- logger.trace("(HTTPS-A)");
+ logger.trace("(HTTPS-ACCEPT)");
if (engine == null) {
return delegate.initialKeyOps();
}
- return handshakeState == ProcessorState.Read ? SelectionKey.OP_READ : SelectionKey.OP_WRITE;
+ return toProcessorState() == ProcessorState.Read ? SelectionKey.OP_READ : SelectionKey.OP_WRITE;
}
/**
@@ -140,87 +147,76 @@ public long lastUsed() {
return delegate.lastUsed();
}
+ // TODO : at the end of handshake, the final read contains 90 bytes of handshake and 144 bytes of body
+ // then, we actually write the ack back to the the client of the handshake completing. During this ACK is when
+ // delete that body stuff
@Override
public ProcessorState read(ByteBuffer buffer) throws IOException {
+ logger.trace("(HTTPS-READ) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
delegate.markUsed();
+ // HTTPS is disabled
if (engine == null) {
return delegate.read(buffer);
}
- var tlsStatus = engine.getHandshakeStatus();
- ByteBuffer decryptBuffer;
- if (tlsStatus != HandshakeStatus.NOT_HANDSHAKING && tlsStatus != HandshakeStatus.FINISHED) {
- logger.trace("(HTTPS-R-HS)" + tlsStatus);
- decryptBuffer = peerAppData.clear();
- } else {
- logger.trace("(HTTPS-R-RQ)");
- handshakeState = null;
- decryptBuffer = delegate.readBuffer();
- }
+ if (state == HTTPSState.HandshakeRead || state == HTTPSState.HandshakeWrite) {
+ state = handshake();
- // TODO : Not sure if this is correct
- if (decryptBuffer == null) {
- logger.trace("(HTTPS-R-NULL)");
- return delegate.state();
- }
+ if (handshakeData.hasRemaining()) {
+ // This shouldn't happen, but let's resize just in case
+ if (handshakeData.remaining() > encryptedData.remaining()) {
+ logger.trace("(HTTPS-READ-RESIZE-AFTER-HANDSHAKE-BEFORE) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ encryptedData = resizeBuffer(encryptedData, encryptedData.capacity() + handshakeData.remaining());
+ logger.trace("(HTTPS-READ-RESIZE-AFTER-HANDSHAKE-AFTER) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ }
- // Unwrapping using one of these cases:
- // Handshake Data (plain text or encrypted/signed) ---> Ignore (this side doesn't matter as it is internal to SSLEngine)
- // * If OK - might need TASK
- // * If Underflow - Handshake Data was not enough or there isn't enough space in the buffer
- // * If Overflow - ??
- // Encrypted Data ---> Plain Text Data (send to the HTTP handler)
- // * If OK - send the decrypted data to the app
- // * If Underflow - Encrypted Data was not enough or there isn't enough space in the network buffer
- // * If Overflow - The encrypted data was larger than the buffer the app is using (Preamble or body buffers)
- var result = engine.unwrap(peerNetData, decryptBuffer);
-
- // This will always put position at limit, so if there is data in the buffer, it will always be at the start and position will be greater than 0
- // Therefore, we will need to flip this if we are resizing (i.e. for an underflow)
- peerNetData.compact();
-
- if (result.getStatus() == Status.BUFFER_UNDERFLOW) {
- logger.trace("(HTTPS-R-UF)");
- peerNetData = handleBufferUnderflow(peerNetData);
- return handshakeState != null ? handshakeState : delegate.state(); // Keep reading
- } else if (result.getStatus() == Status.CLOSED) {
- logger.trace("(HTTPS-R-C)");
- return close(false);
- } else if (result.getStatus() == Status.BUFFER_OVERFLOW) {
- throw new IllegalStateException("A buffer overflow is not expected during an unwrap operation. This occurs because the preamble or body buffers are too small. Increase their sizes to avoid this issue.");
- }
+ encryptedData.put(handshakeData);
+ logger.trace("(HTTPS-READ-COPY-AFTER-HANDSHAKE) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ }
- if (tlsStatus == HandshakeStatus.NOT_HANDSHAKING || tlsStatus == HandshakeStatus.FINISHED) {
- logger.trace("(HTTPS-R-RQ-R)");
- handshakeState = null;
- decryptBuffer.flip();
- return delegate.read(decryptBuffer);
+ // We've got more handshaking to do
+ if (state == HTTPSState.HandshakeRead || state == HTTPSState.HandshakeWrite) {
+ handshakeData.clear();
+ return toProcessorState();
+ }
+
+ if (encryptedData.hasRemaining()) {
+ // We are no longer handshaking, but there are bytes, which we should assume are body bytes and fall through to the code below
+ state = HTTPSState.BodyRead;
+ encryptedData.compact();
+ } else {
+ // We are done handshaking, clear the buffer to start the body read
+ encryptedData.clear();
+ }
}
- logger.trace("(HTTPS-HS-UW){}", peerNetData);
- var newTLSStatus = result.getHandshakeStatus();
- var newState = handleHandshake(newTLSStatus);
+ decrypt();
- // Sometimes the peer network side still has more handshake data and/or request data, so we can recurse to handle whatever is remaining
- if (handshakeState == ProcessorState.Read && peerNetData.position() > 0 && result.bytesConsumed() > 0) {
- peerNetData.flip();
- return read(peerNetData);
- }
+ // Check if we are done reading
+ state = switch (delegate.state()) {
+ case Read -> HTTPSState.BodyRead;
+ case Write -> HTTPSState.BodyWrite;
+ case Close -> HTTPSState.Close;
+ case Reset -> HTTPSState.Reset;
+ };
- return newState;
+ logger.trace("(HTTPS-READ-DONE) {} {} {}", encryptedData, decryptedData, state);
+ return toProcessorState();
}
@Override
public ByteBuffer readBuffer() {
+ logger.trace("(HTTPS-READ-BUFFER) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
delegate.markUsed();
+ // HTTPS is disabled
if (engine == null) {
return delegate.readBuffer();
}
// Always read into the peer network buffer
- return peerNetData;
+ return state == HTTPSState.HandshakeRead ? handshakeData : encryptedData;
}
@Override
@@ -230,68 +226,62 @@ public long readThroughput() {
@Override
public ProcessorState state() {
+ // HTTPS is disabled
if (engine == null) {
return delegate.state();
}
- return handshakeState != null ? handshakeState : delegate.state();
+ return toProcessorState();
}
+ /**
+ * Updates the delegate in order to reset the state of it (HTTP state machine). This also resets the TLS state back to a BodyRead
+ *
+ * @param delegate The new delegate.
+ */
public void updateDelegate(HTTP11Processor delegate) {
this.delegate = delegate;
+ this.state = HTTPSState.BodyRead;
+
+ // Reset all the buffers since this is a new request/response cycle
+ if (engine != null) {
+ this.decryptedData.clear();
+ this.encryptedData.clear();
+ this.handshakeData.clear();
+ }
}
@Override
public ByteBuffer[] writeBuffers() throws IOException {
+ logger.trace("(HTTPS-WRITE-BUFFERS) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
delegate.markUsed();
+ // HTTPS is disabled
if (engine == null) {
return delegate.writeBuffers();
}
- // We haven't written it all out yet, so return the existing bytes in the encrypted/handshake buffer
- if (myNetData[0].hasRemaining()) {
- return myNetData;
+ // We haven't written it all out yet, so return the existing bytes in the encrypted or handshake buffer
+ if (state == HTTPSState.HandshakeWriting && handshakeData.hasRemaining()) {
+ return handshakeDataArray;
}
- var tlsStatus = engine.getHandshakeStatus();
- if (tlsStatus == HandshakeStatus.NEED_UNWRAP) {
- handshakeState = ProcessorState.Read;
- return null;
+ if (state == HTTPSState.BodyWriting && encryptedData.hasRemaining()) {
+ return encryptedDataArray;
}
- // TODO : can bytes be in the handshake that clear() doesn't even provide enough protection
- ByteBuffer[] plainTextBuffers;
- if (tlsStatus == HandshakeStatus.NEED_WRAP) {
- logger.trace("(HTTPS-W-HS)");
- myAppData[0].clear(); // TODO : Always clear for the handshake??
- plainTextBuffers = myAppData;
+ if (state == HTTPSState.HandshakeRead || state == HTTPSState.HandshakeWrite) {
+ state = handshake();
} else {
- handshakeState = null;
- plainTextBuffers = delegate.writeBuffers();
+ encrypt();
}
- if (plainTextBuffers == null) {
+ // If we aren't writing, bail
+ if (state != HTTPSState.BodyWriting && state != HTTPSState.HandshakeWriting) {
return null;
}
- myNetData[0].clear();
- var result = engine.wrap(plainTextBuffers, myNetData[0]);
- if (result.getStatus() == Status.BUFFER_OVERFLOW) {
- logger.trace("(HTTPS-W-OF)");
- myNetData[0] = handleBufferOverflow(myNetData[0]);
- } else if (result.getStatus() == Status.CLOSED) {
- logger.trace("(HTTPS-W-C)");
- close(false);
- return null;
- } else if (result.getStatus() == Status.BUFFER_UNDERFLOW) {
- throw new IllegalStateException("A buffer underflow is not expected during a wrap operation according to the Javadoc. Maybe this is something we need to fix.");
- } else {
- logger.trace("(HTTPS-W-RQ)");
- myNetData[0].flip();
- }
-
- return myNetData;
+ return state == HTTPSState.HandshakeWriting ? handshakeDataArray : encryptedDataArray;
}
@Override
@@ -303,77 +293,297 @@ public long writeThroughput() {
public ProcessorState wrote(long num) throws IOException {
delegate.markUsed();
- if (handshakeState == null) {
+ // HTTPS is disabled
+ if (engine == null) {
return delegate.wrote(num);
}
- var tlsStatus = engine.getHandshakeStatus();
- return handleHandshake(tlsStatus);
+ logger.trace("(HTTPS-WROTE) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+
+ if (state == HTTPSState.HandshakeWriting && !handshakeData.hasRemaining()) {
+ handshakeData.clear();
+
+ var handshakeStatus = engine.getHandshakeStatus();
+ state = switch (handshakeStatus) {
+ case NEED_WRAP -> HTTPSState.HandshakeWrite;
+ case NEED_UNWRAP, NEED_UNWRAP_AGAIN -> HTTPSState.HandshakeRead;
+ case FINISHED, NOT_HANDSHAKING -> HTTPSState.BodyRead;
+ default -> throw new IllegalStateException("Handshaking went from write to task, which was unexpected");
+ };
+
+ // This means we had body data during handshaking, and we need to handle it here
+ if (state == HTTPSState.BodyRead && encryptedData.position() > 0) {
+ encryptedData.flip();
+ read(encryptedData);
+ }
+ } else {
+ // Check if we are done writing
+ var newState = delegate.wrote(num);
+ state = switch (newState) {
+ case Read -> HTTPSState.BodyRead;
+ case Write -> HTTPSState.BodyWrite;
+ case Close -> HTTPSState.Close;
+ case Reset -> HTTPSState.Reset;
+ };
+
+ if (state == HTTPSState.BodyRead) {
+ // This condition should never happen because the write-operation should have written out the entire buffer
+ if (encryptedData.hasRemaining()) {
+ throw new IllegalStateException("The encrypted data still has data to write, but the HTTP processor changed states.");
+ }
+
+ // Clear the encrypted side of the buffer to start the read
+ encryptedData.clear();
+ }
+ }
+
+ logger.trace("(HTTPS-WROTE-DONE) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ return toProcessorState();
}
- private ByteBuffer handleBufferOverflow(ByteBuffer buffer) {
- int applicationSize = engine.getSession().getApplicationBufferSize();
- ByteBuffer newBuffer = ByteBuffer.allocate(applicationSize + buffer.position());
- buffer.flip();
- newBuffer.put(buffer);
- return newBuffer;
+ private void copyToDelegate() throws IOException {
+ decryptedData.flip();
+
+ while (decryptedData.hasRemaining()) {
+ var buf = delegate.readBuffer();
+ if (buf == null) {
+ logger.trace("(HTTPS-DECRYPT-COPY-TO-DELEGATE-NULL)");
+ throw new ParseException("Unable to complete HTTP request because the server thought the request was complete but the client sent more data");
+ }
+
+ logger.trace("(HTTPS-DECRYPT-COPY-TO-DELEGATE) {} {} {}", encryptedData, decryptedData, state);
+
+ // Copy it
+ int length = Math.min(buf.remaining(), decryptedData.remaining());
+ buf.put(buf.position(), decryptedData, decryptedData.position(), length);
+ buf.position(buf.position() + length);
+ buf.flip();
+ decryptedData.position(decryptedData.position() + length);
+ logger.trace("(HTTPS-DECRYPT-COPY-TO-DELEGATE-COPIED) {} {} {}", encryptedData, decryptedData, state);
+
+ // Pass it down
+ var newState = delegate.read(buf);
+ logger.trace("(HTTPS-DECRYPT-COPY-TO-DELEGATE-DONE) {} {} {} {}", encryptedData, decryptedData, state, newState);
+ }
}
- private ByteBuffer handleBufferUnderflow(ByteBuffer buffer) {
- int networkSize = engine.getSession().getPacketBufferSize();
- if (networkSize > buffer.capacity()) {
- ByteBuffer newBuffer = ByteBuffer.allocate(networkSize);
- buffer.flip();
- newBuffer.put(buffer);
- buffer = newBuffer;
+ private void decrypt() throws IOException {
+// var handshakeStatus = engine.getHandshakeStatus();
+// if (handshakeStatus != HandshakeStatus.FINISHED && handshakeStatus != HandshakeStatus.NOT_HANDSHAKING) {
+// throw new IllegalStateException("Unexpected handshake after the connection was in the body processing state.");
+// }
+
+ if (state != HTTPSState.BodyRead) {
+ throw new IllegalStateException("Somehow we got into a state of [" + state + "] but should be in BodyRead.");
}
- return buffer;
+ boolean overflowedAlready = false;
+ SSLEngineResult result;
+ while (encryptedData.hasRemaining()) {
+ logger.trace("(HTTPS-DECRYPT-BEFORE) {} {} {}", encryptedData, decryptedData, state);
+ result = engine.unwrap(encryptedData, decryptedData);
+ logger.trace("(HTTPS-DECRYPT-AFTER) {} {} {}", encryptedData, decryptedData, state);
+
+ Status status = result.getStatus();
+ if (status == Status.BUFFER_OVERFLOW) {
+ logger.trace("(HTTPS-DECRYPT-OVERFLOW) {} {} {}", encryptedData, decryptedData, state);
+ if (overflowedAlready) {
+ throw new IllegalStateException("We already overflowed the decryption buffer and resized it, so this is extremely unexpected.");
+ }
+
+ overflowedAlready = true;
+ decryptedData = resizeBuffer(decryptedData, engine.getSession().getApplicationBufferSize());
+ continue;
+ }
+
+ if (status == Status.BUFFER_UNDERFLOW) {
+ logger.trace("(HTTPS-DECRYPT-UNDERFLOW-BEFORE) {} {} {}", encryptedData, decryptedData, state);
+ encryptedData.compact(); // Compact for good measure and then go read some more
+ logger.trace("(HTTPS-DECRYPT-UNDERFLOW-AFTER) {} {} {}", encryptedData, decryptedData, state);
+ return;
+ }
+
+ if (status == Status.CLOSED) {
+ logger.trace("(HTTPS-DECRYPT-CLOSE) {} {} {}", encryptedData, decryptedData, state);
+ state = HTTPSState.Close;
+ return;
+ }
+
+ copyToDelegate();
+ decryptedData.clear(); // Should have been fully drained to delegate
+ }
+
+ encryptedData.clear();
}
- private ProcessorState handleHandshake(HandshakeStatus newTLSStatus) throws IOException {
- if (newTLSStatus == HandshakeStatus.NEED_UNWRAP_AGAIN) {
- throw new IllegalStateException("The NEED_UNWRAP_AGAIN state should not happen in HTTPS");
+ private void encrypt() throws SSLException {
+ logger.trace("(HTTPS-ENCRYPT) {} {} {}", encryptedData, decryptedData, state);
+ var buffers = delegate.writeBuffers();
+ if (buffers == null || buffers.length == 0) {
+ return;
}
- if (newTLSStatus == HandshakeStatus.NEED_TASK) {
- logger.trace("(HTTPS-HS-T)");
+ // This is safe because we only get here when there are no more bytes remaining to write
+ encryptedData.clear();
+ logger.trace("(HTTPS-ENCRYPT-CLEAR) {} {} {}", encryptedData, decryptedData, state);
+
+ Status status;
+ do {
+ logger.trace("(HTTPS-ENCRYPT-WRAP-BEFORE) {} {} {}", encryptedData, decryptedData, state);
+ var result = engine.wrap(buffers, encryptedData);
+ status = result.getStatus();
+ logger.trace("(HTTPS-ENCRYPT-WRAP-AFTER) {} {} {} {}", encryptedData, decryptedData, state, status);
+ // We don't have enough bytes from the application, let the worker thread keep processing and we'll be back
+ if (status == Status.BUFFER_UNDERFLOW) {
+ return;
+ }
+
+ if (status == Status.CLOSED) {
+ state = HTTPSState.Close;
+ return;
+ }
+
+ if (status == Status.BUFFER_OVERFLOW) {
+ encryptedData = resizeBuffer(encryptedData, engine.getSession().getPacketBufferSize());
+ logger.trace("(HTTPS-ENCRYPT-RESIZE) {} {} {}", encryptedData, decryptedData, state);
+ }
+ } while (status == Status.BUFFER_OVERFLOW);
+
+ encryptedData.flip();
+ state = HTTPSState.BodyWriting;
+ }
+
+ private HandshakeStatus handleHandshakeTask(HandshakeStatus handshakeStatus) {
+ if (handshakeStatus == HandshakeStatus.NEED_TASK) {
+ logger.trace("(HTTPS-HANDSHAKE-TASK) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
// Keep hard looping until the thread finishes (sucks but not sure what else to do here)
- // TODO : is this sucky?
do {
- newTLSStatus = engine.getHandshakeStatus();
-
Runnable task;
while ((task = engine.getDelegatedTask()) != null) {
- executor.submit(task);
+ task.run();
}
- } while (newTLSStatus == HandshakeStatus.NEED_TASK);
+
+ handshakeStatus = engine.getHandshakeStatus();
+ } while (handshakeStatus == HandshakeStatus.NEED_TASK);
+
+ logger.trace("(HTTPS-HANDSHAKE-TASK-DONE) {} {} {} {} {}", handshakeData, encryptedData, decryptedData, state, handshakeStatus);
}
- if (newTLSStatus == HandshakeStatus.NEED_UNWRAP) {
- logger.trace("(HTTPS-HS-R)");
- handshakeState = ProcessorState.Read;
- } else if (newTLSStatus == HandshakeStatus.NEED_WRAP) {
- logger.trace("(HTTPS-HS-W)");
- handshakeState = ProcessorState.Write;
- } else {
- logger.trace("(HTTPS-HS-DONE)" + newTLSStatus.name());
+ return handshakeStatus;
+ }
+
+ private HTTPSState handshake() throws SSLException {
+ var handshakeStatus = engine.getHandshakeStatus();
+ if (handshakeStatus == HandshakeStatus.FINISHED || handshakeStatus == HandshakeStatus.NOT_HANDSHAKING) {
+ return HTTPSState.BodyRead;
+ }
+
+ handshakeStatus = handleHandshakeTask(handshakeStatus);
- if (!myNetData[0].hasRemaining()) {
- logger.trace("(HTTPS-HS-DONE)" + newTLSStatus.name() + "-" + delegate.state());
- handshakeState = null;
+ if (handshakeStatus == HandshakeStatus.NEED_UNWRAP || handshakeStatus == HandshakeStatus.NEED_UNWRAP_AGAIN) {
+ logger.trace("(HTTPS-HANDSHAKE-UNWRAP) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ SSLEngineResult result = null;
+ while ((handshakeStatus == HandshakeStatus.NEED_UNWRAP || handshakeStatus == HandshakeStatus.NEED_UNWRAP_AGAIN) && handshakeData.hasRemaining()) {
+ logger.trace("(HTTPS-HANDSHAKE-UNWRAP-BEFORE) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ result = engine.unwrap(handshakeData, decryptedData);
+ logger.trace("(HTTPS-HANDSHAKE-UNWRAP-AFTER) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
- // This indicates that the client sent along part of the HTTP request preamble with its last handshake. We need to consume that before we continue
- if (peerNetData.position() > 0) {
- peerNetData.flip();
- return read(peerNetData);
+ Status status = result.getStatus();
+ if (status == Status.BUFFER_OVERFLOW) {
+ throw new IllegalStateException("Handshake reading should never overflow the network buffer. It is sized such that it can handle a full TLS packet.");
}
- return delegate.state();
+ if (status == Status.BUFFER_UNDERFLOW) {
+ logger.trace("(HTTPS-HANDSHAKE-UNWRAP-UNDERFLOW) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ handshakeData.compact(); // Compact for good measure and then go read some more
+ return HTTPSState.HandshakeRead;
+ }
+
+ if (status == Status.CLOSED) {
+ logger.trace("(HTTPS-HANDSHAKE-UNWRAP-CLOSE) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ return HTTPSState.Close;
+ }
+
+ handshakeStatus = result.getHandshakeStatus();
+ handshakeStatus = handleHandshakeTask(handshakeStatus); // In case the handshake immediately went into a NEED_TASK mode
+ }
+
+ // We never had any bytes to handle, bail!
+ if (result == null) {
+ logger.trace("(HTTPS-HANDSHAKE-UNWRAP-EMPTY) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ return state;
+ }
+
+ logger.trace("(HTTPS-HANDSHAKE-DONE) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ return switch (handshakeStatus) {
+ case NEED_WRAP -> HTTPSState.HandshakeWrite;
+ case NEED_UNWRAP, NEED_UNWRAP_AGAIN -> HTTPSState.HandshakeRead;
+ case FINISHED, NOT_HANDSHAKING -> HTTPSState.BodyRead;
+ default -> throw new IllegalStateException("Handshaking got back into a NEED_TASK mode and should have handled that above.");
+ };
+ } else if (handshakeStatus == HandshakeStatus.NEED_WRAP) {
+ logger.trace("(HTTPS-HANDSHAKE-WRAP) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ SSLEngineResult result;
+ Status status;
+ do {
+ logger.trace("(HTTPS-HANDSHAKE-WRAP-BEFORE) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ result = engine.wrap(decryptedData, handshakeData);
+ status = result.getStatus();
+ logger.trace("(HTTPS-HANDSHAKE-WRAP-AFTER) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ if (status == Status.BUFFER_OVERFLOW) {
+ logger.trace("(HTTPS-HANDSHAKE-WRAP-OVERFLOW) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ handshakeData = resizeBuffer(handshakeData, engine.getSession().getPacketBufferSize() + handshakeData.remaining());
+ }
+ } while (status == Status.BUFFER_OVERFLOW);
+
+ if (status == Status.BUFFER_UNDERFLOW) {
+ throw new IllegalStateException("Handshake writing should never underflow the network buffer. The engine handles generating handshake data, so this should be impossible.");
+ }
+
+ if (status == Status.CLOSED) {
+ logger.trace("(HTTPS-HANDSHAKE-WRAP-CLOSE) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ return HTTPSState.Close;
}
+
+ // Should have the next chunk of handshake data to write, so transition to the writing mode
+ handshakeData.flip();
+ logger.trace("(HTTPS-HANDSHAKE-DONE) {} {} {} {}", handshakeData, encryptedData, decryptedData, state);
+ return HTTPSState.HandshakeWriting;
}
- return handshakeState;
+ return state;
+ }
+
+ private ByteBuffer resizeBuffer(ByteBuffer buffer, int engineSize) {
+ if (engineSize > buffer.capacity()) {
+ ByteBuffer newBuffer = ByteBuffer.allocate(engineSize + buffer.remaining());
+ newBuffer.put(buffer);
+ buffer = newBuffer;
+ } else {
+ buffer.compact();
+ }
+
+ return buffer;
+ }
+
+ private ProcessorState toProcessorState() {
+ return switch (state) {
+ case BodyRead, HandshakeRead -> ProcessorState.Read;
+ case BodyWrite, BodyWriting, HandshakeWrite, HandshakeWriting -> ProcessorState.Write;
+ case Close -> ProcessorState.Close;
+ case Reset -> ProcessorState.Reset;
+ };
+ }
+
+ public enum HTTPSState {
+ BodyRead,
+ BodyWrite,
+ BodyWriting,
+ Close,
+ HandshakeRead,
+ HandshakeWrite,
+ HandshakeWriting,
+ Reset,
}
}
diff --git a/src/main/java/io/fusionauth/http/server/HTTPServerThread.java b/src/main/java/io/fusionauth/http/server/HTTPServerThread.java
index 536bcdb..2c8df30 100644
--- a/src/main/java/io/fusionauth/http/server/HTTPServerThread.java
+++ b/src/main/java/io/fusionauth/http/server/HTTPServerThread.java
@@ -27,7 +27,6 @@
import java.nio.channels.SocketChannel;
import java.security.GeneralSecurityException;
import java.time.Duration;
-import java.util.List;
import java.util.Map;
import io.fusionauth.http.ClientAbortException;
@@ -127,33 +126,33 @@ public void notifyNow() {
return;
}
- // Update the keys based on any changes to the processor state
- var keys = List.copyOf(selector.keys());
- for (SelectionKey key : keys) {
- try {
- // If the key is toast, skip it
- if (!key.isValid()) {
- continue;
- }
-
- HTTPProcessor processor = (HTTPProcessor) key.attachment();
- if (processor == null) { // No processor for you!
- continue;
- }
-
- ProcessorState state = processor.state();
- if (state == ProcessorState.Read && key.interestOps() != SelectionKey.OP_READ) {
- logger.debug("Flipping a SelectionKey to Read because it wasn't in the right state");
- key.interestOps(SelectionKey.OP_READ);
- } else if (state == ProcessorState.Write && key.interestOps() != SelectionKey.OP_WRITE) {
- logger.debug("Flipping a SelectionKey to Write because it wasn't in the right state");
- key.interestOps(SelectionKey.OP_WRITE);
- }
- } catch (Throwable t) {
- // Smother since the key is likely invalid
- logger.debug("Exception occurred while trying to update a key", t);
- }
- }
+// // Update the keys based on any changes to the processor state
+// var keys = List.copyOf(selector.keys());
+// for (SelectionKey key : keys) {
+// try {
+// // If the key is toast, skip it
+// if (!key.isValid()) {
+// continue;
+// }
+//
+// HTTPProcessor processor = (HTTPProcessor) key.attachment();
+// if (processor == null) { // No processor for you!
+// continue;
+// }
+//
+// ProcessorState state = processor.state();
+// if (state == ProcessorState.Read && key.interestOps() != SelectionKey.OP_READ) {
+// logger.debug("Flipping a SelectionKey to Read because it wasn't in the right state");
+// key.interestOps(SelectionKey.OP_READ);
+// } else if (state == ProcessorState.Write && key.interestOps() != SelectionKey.OP_WRITE) {
+// logger.debug("Flipping a SelectionKey to Write because it wasn't in the right state");
+// key.interestOps(SelectionKey.OP_WRITE);
+// }
+// } catch (Throwable t) {
+// // Smother since the key is likely invalid
+// logger.debug("Exception occurred while trying to update a key", t);
+// }
+// }
// Wake-up! Time to put on a little make-up!
selector.wakeup();
@@ -235,28 +234,45 @@ private void accept(SelectionKey key) throws GeneralSecurityException, IOExcepti
}
private void cancelAndCloseKey(SelectionKey key) {
- if (key != null) {
- try (var client = key.channel()) {
- if (logger.isDebugEnabled() && client instanceof SocketChannel socketChannel) {
- logger.debug("Closing connection to client [{}]", socketChannel.getRemoteAddress().toString());
- }
+ if (key == null) {
+ return;
+ }
- // Close the processor, which should kill the thread
- if (key.attachment() != null) {
- ((HTTPProcessor) key.attachment()).close(false);
- }
+ try (var client = key.channel()) {
+ if (logger.isDebugEnabled() && client instanceof SocketChannel socketChannel) {
+ logger.debug("Closing connection to client [{}]", socketChannel.getRemoteAddress().toString());
+ }
- key.cancel();
+ // Close the processor, which should kill the thread
+ if (key.attachment() != null) {
+ ProcessorState state = ((HTTPProcessor) key.attachment()).close(true);
- if (client.validOps() != SelectionKey.OP_ACCEPT && instrumenter != null) {
- instrumenter.connectionClosed();
+ // Handle state transitions here in case there is more data to write/read. If that is the case, then we should return rather than
+ // cancel the key
+ if (state == ProcessorState.Read) {
+ logger.trace("(HTTP-SERVER-CLOSE-READ)");
+ key.interestOps(SelectionKey.OP_READ);
+ return;
+ }
+
+ if (state == ProcessorState.Write) {
+ logger.trace("(HTTP-SERVER-CLOSE-WRITE)");
+ key.interestOps(SelectionKey.OP_WRITE);
+ return;
}
- } catch (Throwable t) {
- logger.error("An exception was thrown while trying to cancel a SelectionKey and close a channel with a client due to an exception being thrown for that specific client. Enable debug logging to see the error", t);
}
- logger.trace("(C)");
+ client.close();
+ key.cancel();
+
+ if (client.validOps() != SelectionKey.OP_ACCEPT && instrumenter != null) {
+ instrumenter.connectionClosed();
+ }
+ } catch (Throwable t) {
+ logger.error("An exception was thrown while trying to cancel a SelectionKey and close a channel with a client due to an exception being thrown for that specific client. Enable debug logging to see the error", t);
}
+
+ logger.trace("(C)");
}
@SuppressWarnings("resource")
@@ -326,6 +342,7 @@ private String ipAddress(SocketChannel client) throws IOException {
private void read(SelectionKey key) throws IOException {
HTTPProcessor processor = (HTTPProcessor) key.attachment();
ProcessorState state = processor.state();
+ logger.trace("(R) {}", state);
SocketChannel client = (SocketChannel) key.channel();
if (state == ProcessorState.Read) {
ByteBuffer buffer = processor.readBuffer();
@@ -380,7 +397,7 @@ private void write(SelectionKey key) throws IOException {
if (num < 0) {
logger.debug("Client refused bytes or terminated the connection. Num bytes is [{}]. Closing connection", num);
- state = processor.close(true);
+ processor.close(true); // Don't use the new state because the socket is toast
} else {
if (num > 0) {
logger.debug("Wrote [{}] bytes to the client", num);
diff --git a/src/test/java/io/fusionauth/http/BaseTest.java b/src/test/java/io/fusionauth/http/BaseTest.java
index 6fef705..7a28435 100644
--- a/src/test/java/io/fusionauth/http/BaseTest.java
+++ b/src/test/java/io/fusionauth/http/BaseTest.java
@@ -15,6 +15,7 @@
*/
package io.fusionauth.http;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -23,6 +24,9 @@
import java.net.Socket;
import java.net.URI;
import java.net.http.HttpClient;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.KeyPair;
@@ -123,6 +127,8 @@ public abstract class BaseTest {
static {
logger.setLevel(Level.Trace);
+ System.setProperty("sun.net.http.retryPost", "false");
+ System.setProperty("jdk.httpclient.allowRestrictedHeaders", "connection");
}
public HttpClient makeClient(String scheme, CookieHandler cookieHandler) throws GeneralSecurityException, IOException {
@@ -351,21 +357,25 @@ public void onTestFailure(ITestResult result) {
String trace = logger.toString();
// Intentionally leaving empty lines here
- System.out.println("""
+ try {
+ Files.write(Paths.get("output.txt"), """
+
+
+
+ Test failure
+ -----------------
+ Exception: {{exception}}
+ Message: {{message}}
-
-
- Test failure
- -----------------
- Exception: {{exception}}
- Message: {{message}}
-
- HTTP Trace:
- {{trace}}
- -----------------
- """.replace("{{exception}}", throwable != null ? throwable.getClass().getSimpleName() : "-")
- .replace("{{message}}", throwable != null ? (throwable.getMessage() != null ? throwable.getMessage() : "-") : "-")
- .replace("{{trace}}", trace));
+ HTTP Trace:
+ {{trace}}
+ -----------------
+ """.replace("{{exception}}", throwable != null ? throwable.getClass().getSimpleName() : "-")
+ .replace("{{message}}", throwable != null ? (throwable.getMessage() != null ? throwable.getMessage() : "-") : "-")
+ .replace("{{trace}}", trace).getBytes(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
}
@Override
diff --git a/src/test/java/io/fusionauth/http/ChunkedTest.java b/src/test/java/io/fusionauth/http/ChunkedTest.java
index 92d8920..bf28fa8 100644
--- a/src/test/java/io/fusionauth/http/ChunkedTest.java
+++ b/src/test/java/io/fusionauth/http/ChunkedTest.java
@@ -52,11 +52,6 @@ public class ChunkedTest extends BaseTest {
public static final String RequestBody = "{\"message\":\"Hello World\"";
- static {
- System.setProperty("sun.net.http.retryPost", "false");
- System.setProperty("jdk.httpclient.allowRestrictedHeaders", "connection");
- }
-
@Test(dataProvider = "schemes")
public void chunkedRequest(String scheme) throws Exception {
HTTPHandler handler = (req, res) -> {
@@ -262,6 +257,9 @@ public void performanceChunked(String scheme) throws Exception {
if (i % 1_000 == 0) {
System.out.println(i);
}
+
+ // Wipe the logger, so we only have the final failed request
+ resetLogger();
}
long end = System.currentTimeMillis();
diff --git a/src/test/java/io/fusionauth/http/CoreTest.java b/src/test/java/io/fusionauth/http/CoreTest.java
index ca0fdfc..d0175e9 100644
--- a/src/test/java/io/fusionauth/http/CoreTest.java
+++ b/src/test/java/io/fusionauth/http/CoreTest.java
@@ -67,11 +67,6 @@ public class CoreTest extends BaseTest {
public static final String RequestBody = "{\"message\":\"Hello World\"";
- static {
- System.setProperty("sun.net.http.retryPost", "false");
- System.setProperty("jdk.httpclient.allowRestrictedHeaders", "connection");
- }
-
@Test(dataProvider = "schemes")
public void badLanguage(String scheme) throws Exception {
HTTPHandler handler = (req, res) -> {
@@ -323,8 +318,8 @@ public void keepAliveTimeout() {
if (response.status != 200) {
System.out.println(response.exception);
}
-
assertEquals(response.status, 200);
+
response = new RESTClient<>(Void.TYPE, Void.TYPE)
.url(uri.toString())
.connectTimeout(0)
@@ -335,20 +330,23 @@ public void keepAliveTimeout() {
if (response.status != 200) {
System.out.println(response.exception);
}
-
assertEquals(response.status, 200);
}
}
@Test
public void logger() {
- // Test replacement values and ensure we are handling special regex characters.
- logger.setLevel(Level.Debug);
- logger.info("Class name: [{}]", "io.fusionauth.http.Test$InnerClass");
+ try {
+ // Test replacement values and ensure we are handling special regex characters.
+ logger.setLevel(Level.Debug);
+ logger.info("Class name: [{}]", "io.fusionauth.http.Test$InnerClass");
- // Expect that we do not encounter an exception.
- String output = logger.toString();
- assertTrue(output.endsWith("Class name: [io.fusionauth.http.Test$InnerClass]"));
+ // Expect that we do not encounter an exception.
+ String output = logger.toString();
+ assertTrue(output.endsWith("Class name: [io.fusionauth.http.Test$InnerClass]"));
+ } finally {
+ logger.setLevel(Level.Trace);
+ }
}
@Test(dataProvider = "schemes", groups = "performance")
@@ -385,6 +383,9 @@ public void performance(String scheme) throws Exception {
if (i % 1_000 == 0) {
System.out.println(i);
}
+
+ // Wipe the logger, so we only have the final failed request
+ resetLogger();
}
long end = System.currentTimeMillis();
@@ -411,20 +412,29 @@ public void performanceNoKeepAlive(String scheme) throws Exception {
}
};
- int iterations = 1_000;
+ int iterations = 10_000;
CountingInstrumenter instrumenter = new CountingInstrumenter();
try (HTTPServer ignore = makeServer(scheme, handler, instrumenter).start()) {
URI uri = makeURI(scheme, "");
var client = makeClient(scheme, null);
long start = System.currentTimeMillis();
for (int i = 0; i < iterations; i++) {
+ System.out.println("Iteration " + i + " start " + System.currentTimeMillis());
var response = client.send(
- HttpRequest.newBuilder().uri(uri).header(Headers.Connection, Connections.Close).GET().build(),
+ HttpRequest.newBuilder()
+ .uri(uri)
+ .header(Headers.Connection, Connections.Close)
+ .POST(BodyPublishers.noBody())
+ .build(),
r -> BodySubscribers.ofString(StandardCharsets.UTF_8)
);
+ System.out.println("Iteration " + i + " end " + System.currentTimeMillis());
assertEquals(response.statusCode(), 200);
assertEquals(response.body(), ExpectedResponse);
+
+ // Wipe the logger, so we only have the final failed request
+ resetLogger();
}
long end = System.currentTimeMillis();
@@ -711,6 +721,46 @@ public void statusOnly(String scheme) throws Exception {
}
}
+ @Test
+ public void tlsIssues() throws Exception {
+ HTTPHandler handler = (req, res) -> {
+ res.setStatus(200);
+ res.getOutputStream().close();
+ };
+
+ try (HTTPServer ignore = makeServer("https", handler).start()) {
+ var client = makeClient("https", null);
+ URI uri = makeURI("https", "");
+ var body = BodyPublishers.ofByteArray("primeCSRFToken=QkJCAXsbQBgZhSVe1I4dv9B2ZXYDbUtCzAYUwfkvRjUJcUsBosHTeRbpvHgqXPN8TIK8DkSjG6HeaeJ-Yr4oCnXlUIW8T1r_9tVvuxxo38VKucd8gLnC2Mx7h_QuZu9dHEN79Q%3D%3D&tenantId=&tenant.name=Testing2&tenant.issuer=acme.com&tenant.themeId=75a068fd-e94b-451a-9aeb-3ddb9a3b5987&tenant.formConfiguration.adminUserFormId=ff153db4-d233-fcaa-76fd-98649866ff0b&__cb_tenant.usernameConfiguration.unique.enabled=false&tenant.usernameConfiguration.unique.strategy=OnCollision&tenant.usernameConfiguration.unique.numberOfDigits=5&tenant.usernameConfiguration.unique.separator=%23&tenant.connectorPolicies%5B0%5D.connectorId=e3306678-a53a-4964-9040-1c96f36dda72&connectorDomains%5B0%5D=*&tenant.connectorPolicies%5B0%5D.migrate=false&tenant.emailConfiguration.host=smtp.sendgrid.net&tenant.emailConfiguration.port=587&tenant.emailConfiguration.username=apikey&tenant.emailConfiguration.password=&tenant.emailConfiguration.security=TLS&tenant.emailConfiguration.defaultFromEmail=no-reply%40fusionauth.io&tenant.emailConfiguration.defaultFromName=FusionAuth&additionalEmailHeaders=&__cb_tenant.emailConfiguration.debug=false&__cb_tenant.emailConfiguration.verifyEmail=false&tenant.emailConfiguration.verifyEmail=true&__cb_tenant.emailConfiguration.implicitEmailVerificationAllowed=false&tenant.emailConfiguration.implicitEmailVerificationAllowed=true&__cb_tenant.emailConfiguration.verifyEmailWhenChanged=false&tenant.emailConfiguration.verificationEmailTemplateId=7fa81426-42a9-4eb2-ac09-73c044d410b1&tenant.emailConfiguration.emailVerifiedEmailTemplateId=&tenant.emailConfiguration.verificationStrategy=FormField&tenant.emailConfiguration.unverified.behavior=Gated&__cb_tenant.emailConfiguration.unverified.allowEmailChangeWhenGated=false&__cb_tenant.userDeletePolicy.unverified.enabled=false&tenant.userDeletePolicy.unverified.numberOfDaysToRetain=120&tenant.emailConfiguration.emailUpdateEmailTemplateId=&tenant.emailConfiguration.forgotPasswordEmailTemplateId=0502df1e-4010-4b43-b571-d423fce978b2&tenant.emailConfiguration.loginIdInUseOnCreateEmailTemplateId=&tenant.emailConfiguration.loginIdInUseOnUpdateEmailTemplateId=&tenant.emailConfiguration.loginNewDeviceEmailTemplateId=&tenant.emailConfiguration.loginSuspiciousEmailTemplateId=&tenant.emailConfiguration.passwordResetSuccessEmailTemplateId=&tenant.emailConfiguration.passwordUpdateEmailTemplateId=&tenant.emailConfiguration.passwordlessEmailTemplateId=fa6668cb-8569-44df-b0a2-8fcd996df915&tenant.emailConfiguration.setPasswordEmailTemplateId=e160cc59-a73e-4d95-8287-f82e5c541a5c&tenant.emailConfiguration.twoFactorMethodAddEmailTemplateId=&tenant.emailConfiguration.twoFactorMethodRemoveEmailTemplateId=&__cb_tenant.familyConfiguration.enabled=false&tenant.familyConfiguration.maximumChildAge=12&tenant.familyConfiguration.minimumOwnerAge=21&__cb_tenant.familyConfiguration.allowChildRegistrations=false&tenant.familyConfiguration.allowChildRegistrations=true&tenant.familyConfiguration.familyRequestEmailTemplateId=&tenant.familyConfiguration.confirmChildEmailTemplateId=&tenant.familyConfiguration.parentRegistrationEmailTemplateId=&__cb_tenant.familyConfiguration.parentEmailRequired=false&__cb_tenant.familyConfiguration.deleteOrphanedAccounts=false&tenant.familyConfiguration.deleteOrphanedAccountsDays=30&tenant.multiFactorConfiguration.loginPolicy=Enabled&__cb_tenant.multiFactorConfiguration.authenticator.enabled=false&tenant.multiFactorConfiguration.authenticator.enabled=true&__cb_tenant.multiFactorConfiguration.email.enabled=false&tenant.multiFactorConfiguration.email.templateId=61ee368e-018e-4c15-b7a7-47a696648dba&__cb_tenant.multiFactorConfiguration.sms.enabled=false&tenant.multiFactorConfiguration.sms.messengerId=&tenant.multiFactorConfiguration.sms.templateId=&__cb_tenant.webAuthnConfiguration.enabled=false&tenant.webAuthnConfiguration.enabled=true&tenant.webAuthnConfiguration.relyingPartyId=&tenant.webAuthnConfiguration.relyingPartyName=&__cb_tenant.webAuthnConfiguration.debug=false&__cb_tenant.webAuthnConfiguration.bootstrapWorkflow.enabled=false&tenant.webAuthnConfiguration.bootstrapWorkflow.authenticatorAttachmentPreference=any&tenant.webAuthnConfiguration.bootstrapWorkflow.userVerificationRequirement=required&__cb_tenant.webAuthnConfiguration.reauthenticationWorkflow.enabled=false&tenant.webAuthnConfiguration.reauthenticationWorkflow.enabled=true&tenant.webAuthnConfiguration.reauthenticationWorkflow.authenticatorAttachmentPreference=platform&tenant.webAuthnConfiguration.reauthenticationWorkflow.userVerificationRequirement=required&tenant.httpSessionMaxInactiveInterval=172800&tenant.logoutURL=&tenant.oauthConfiguration.clientCredentialsAccessTokenPopulateLambdaId=&tenant.jwtConfiguration.timeToLiveInSeconds=3600&tenant.jwtConfiguration.accessTokenKeyId=aea58f2a-4943-15ed-2190-0aa051200b64&tenant.jwtConfiguration.idTokenKeyId=092dbedc-30af-4149-9c61-b578f2c72f59&tenant.jwtConfiguration.refreshTokenExpirationPolicy=Fixed&tenant.jwtConfiguration.refreshTokenTimeToLiveInMinutes=43200&tenant.jwtConfiguration.refreshTokenSlidingWindowConfiguration.maximumTimeToLiveInMinutes=43200&tenant.jwtConfiguration.refreshTokenUsagePolicy=Reusable&__cb_tenant.jwtConfiguration.refreshTokenRevocationPolicy.onLoginPrevented=false&tenant.jwtConfiguration.refreshTokenRevocationPolicy.onLoginPrevented=true&__cb_tenant.jwtConfiguration.refreshTokenRevocationPolicy.onMultiFactorEnable=false&__cb_tenant.jwtConfiguration.refreshTokenRevocationPolicy.onPasswordChanged=false&tenant.jwtConfiguration.refreshTokenRevocationPolicy.onPasswordChanged=true&tenant.failedAuthenticationConfiguration.userActionId=&tenant.failedAuthenticationConfiguration.tooManyAttempts=5&tenant.failedAuthenticationConfiguration.resetCountInSeconds=60&tenant.failedAuthenticationConfiguration.actionDuration=3&tenant.failedAuthenticationConfiguration.actionDurationUnit=MINUTES&__cb_tenant.failedAuthenticationConfiguration.actionCancelPolicy.onPasswordReset=false&__cb_tenant.failedAuthenticationConfiguration.emailUser=false&__cb_tenant.passwordValidationRules.breachDetection.enabled=false&tenant.passwordValidationRules.breachDetection.matchMode=High&tenant.passwordValidationRules.breachDetection.onLogin=Off&tenant.passwordValidationRules.breachDetection.notifyUserEmailTemplateId=&tenant.passwordValidationRules.minLength=8&tenant.passwordValidationRules.maxLength=256&__cb_tenant.passwordValidationRules.requireMixedCase=false&__cb_tenant.passwordValidationRules.requireNonAlpha=false&__cb_tenant.passwordValidationRules.requireNumber=false&__cb_tenant.minimumPasswordAge.enabled=false&tenant.minimumPasswordAge.seconds=30&__cb_tenant.maximumPasswordAge.enabled=false&tenant.maximumPasswordAge.days=180&__cb_tenant.passwordValidationRules.rememberPreviousPasswords.enabled=false&tenant.passwordValidationRules.rememberPreviousPasswords.count=1&__cb_tenant.passwordValidationRules.validateOnLogin=false&tenant.passwordEncryptionConfiguration.encryptionScheme=salted-pbkdf2-hmac-sha256&tenant.passwordEncryptionConfiguration.encryptionSchemeFactor=24000&__cb_tenant.passwordEncryptionConfiguration.modifyEncryptionSchemeOnLogin=false&__cb_tenant.eventConfiguration.events%5B%27JWTPublicKeyUpdate%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27JWTPublicKeyUpdate%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27JWTRefreshTokenRevoke%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27JWTRefreshTokenRevoke%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27JWTRefresh%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27JWTRefresh%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27GroupCreate%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27GroupCreate%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27GroupCreateComplete%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27GroupDelete%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27GroupDelete%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27GroupDeleteComplete%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27GroupMemberAdd%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27GroupMemberAdd%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27GroupMemberAddComplete%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27GroupMemberRemove%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27GroupMemberRemove%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27GroupMemberRemoveComplete%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27GroupMemberUpdate%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27GroupMemberUpdate%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27GroupMemberUpdateComplete%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27GroupUpdate%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27GroupUpdate%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27GroupUpdateComplete%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserAction%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserBulkCreate%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserBulkCreate%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserCreate%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserCreate%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserCreateComplete%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserDeactivate%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserDeactivate%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserDelete%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserDelete%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserDeleteComplete%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserEmailUpdate%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserEmailVerified%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserEmailVerified%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserIdentityProviderLink%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserIdentityProviderUnlink%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserLoginIdDuplicateOnCreate%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserLoginIdDuplicateOnUpdate%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserLoginFailed%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserLoginFailed%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserLoginNewDevice%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserLoginNewDevice%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserLoginSuccess%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserLoginSuccess%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserLoginSuspicious%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserLoginSuspicious%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserPasswordBreach%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserPasswordBreach%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserPasswordResetSend%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserPasswordResetStart%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserPasswordResetSuccess%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserPasswordUpdate%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserReactivate%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserReactivate%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserRegistrationCreate%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserRegistrationCreate%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserRegistrationCreateComplete%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserRegistrationDelete%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserRegistrationDelete%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserRegistrationDeleteComplete%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserRegistrationUpdate%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserRegistrationUpdate%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserRegistrationUpdateComplete%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserRegistrationVerified%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserRegistrationVerified%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserTwoFactorMethodAdd%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserTwoFactorMethodRemove%27%5D.enabled=false&__cb_tenant.eventConfiguration.events%5B%27UserUpdate%27%5D.enabled=false&tenant.eventConfiguration.events%5B%27UserUpdate%27%5D.transactionType=None&__cb_tenant.eventConfiguration.events%5B%27UserUpdateComplete%27%5D.enabled=false&tenant.externalIdentifierConfiguration.authorizationGrantIdTimeToLiveInSeconds=30&tenant.externalIdentifierConfiguration.changePasswordIdTimeToLiveInSeconds=600&tenant.externalIdentifierConfiguration.deviceCodeTimeToLiveInSeconds=300&tenant.externalIdentifierConfiguration.emailVerificationIdTimeToLiveInSeconds=86400&tenant.externalIdentifierConfiguration.externalAuthenticationIdTimeToLiveInSeconds=300&tenant.externalIdentifierConfiguration.oneTimePasswordTimeToLiveInSeconds=60&tenant.externalIdentifierConfiguration.passwordlessLoginTimeToLiveInSeconds=180&tenant.externalIdentifierConfiguration.pendingAccountLinkTimeToLiveInSeconds=3600&tenant.externalIdentifierConfiguration.registrationVerificationIdTimeToLiveInSeconds=86400&tenant.externalIdentifierConfiguration.samlv2AuthNRequestIdTimeToLiveInSeconds=300&tenant.externalIdentifierConfiguration.setupPasswordIdTimeToLiveInSeconds=86400&tenant.externalIdentifierConfiguration.trustTokenTimeToLiveInSeconds=180&tenant.externalIdentifierConfiguration.twoFactorIdTimeToLiveInSeconds=300&tenant.externalIdentifierConfiguration.twoFactorOneTimeCodeIdTimeToLiveInSeconds=60&tenant.externalIdentifierConfiguration.twoFactorTrustIdTimeToLiveInSeconds=2592000&tenant.externalIdentifierConfiguration.webAuthnAuthenticationChallengeTimeToLiveInSeconds=180&tenant.externalIdentifierConfiguration.webAuthnRegistrationChallengeTimeToLiveInSeconds=180&tenant.externalIdentifierConfiguration.changePasswordIdGenerator.length=32&tenant.externalIdentifierConfiguration.changePasswordIdGenerator.type=randomBytes&tenant.externalIdentifierConfiguration.emailVerificationIdGenerator.length=32&tenant.externalIdentifierConfiguration.emailVerificationIdGenerator.type=randomBytes&tenant.externalIdentifierConfiguration.emailVerificationOneTimeCodeGenerator.length=6&tenant.externalIdentifierConfiguration.emailVerificationOneTimeCodeGenerator.type=randomAlphaNumeric&tenant.externalIdentifierConfiguration.passwordlessLoginGenerator.length=32&tenant.externalIdentifierConfiguration.passwordlessLoginGenerator.type=randomBytes&tenant.externalIdentifierConfiguration.registrationVerificationIdGenerator.length=32&tenant.externalIdentifierConfiguration.registrationVerificationIdGenerator.type=randomBytes&tenant.externalIdentifierConfiguration.registrationVerificationOneTimeCodeGenerator.length=6&tenant.externalIdentifierConfiguration.registrationVerificationOneTimeCodeGenerator.type=randomAlphaNumeric&tenant.externalIdentifierConfiguration.setupPasswordIdGenerator.length=32&tenant.externalIdentifierConfiguration.setupPasswordIdGenerator.type=randomBytes&tenant.externalIdentifierConfiguration.deviceUserCodeIdGenerator.length=6&tenant.externalIdentifierConfiguration.deviceUserCodeIdGenerator.type=randomAlphaNumeric&tenant.externalIdentifierConfiguration.twoFactorOneTimeCodeIdGenerator.length=6&tenant.externalIdentifierConfiguration.twoFactorOneTimeCodeIdGenerator.type=randomDigits&tenant.emailConfiguration.properties=&__cb_tenant.scimServerConfiguration.enabled=false&tenant.scimServerConfiguration.clientEntityTypeId=&tenant.scimServerConfiguration.serverEntityTypeId=&tenant.lambdaConfiguration.scimUserRequestConverterId=&tenant.lambdaConfiguration.scimUserResponseConverterId=&tenant.lambdaConfiguration.scimEnterpriseUserRequestConverterId=&tenant.lambdaConfiguration.scimEnterpriseUserResponseConverterId=&tenant.lambdaConfiguration.scimGroupRequestConverterId=&tenant.lambdaConfiguration.scimGroupResponseConverterId=&scimSchemas=&__cb_tenant.loginConfiguration.requireAuthentication=false&tenant.loginConfiguration.requireAuthentication=true&tenant.accessControlConfiguration.uiIPAccessControlListId=&__cb_tenant.captchaConfiguration.enabled=false&tenant.captchaConfiguration.enabled=true&tenant.captchaConfiguration.captchaMethod=GoogleRecaptchaV2&tenant.captchaConfiguration.siteKey=6LdGKU8kAAAAAJ75pcseAvWyo3cYnQyIU3eGqulg&tenant.captchaConfiguration.secretKey=6LdGKU8kAAAAALqeN2ECaeLOONJduofuRerZBlyI&tenant.captchaConfiguration.threshold=0.5&tenant.ssoConfiguration.deviceTrustTimeToLiveInSeconds=31536000&blockedDomains=&__cb_tenant.rateLimitConfiguration.failedLogin.enabled=false&tenant.rateLimitConfiguration.failedLogin.limit=5&tenant.rateLimitConfiguration.failedLogin.timePeriodInSeconds=60&__cb_tenant.rateLimitConfiguration.forgotPassword.enabled=false&tenant.rateLimitConfiguration.forgotPassword.limit=5&tenant.rateLimitConfiguration.forgotPassword.timePeriodInSeconds=60&__cb_tenant.rateLimitConfiguration.sendEmailVerification.enabled=false&tenant.rateLimitConfiguration.sendEmailVerification.limit=5&tenant.rateLimitConfiguration.sendEmailVerification.timePeriodInSeconds=60&__cb_tenant.rateLimitConfiguration.sendPasswordless.enabled=false&tenant.rateLimitConfiguration.sendPasswordless.limit=5&tenant.rateLimitConfiguration.sendPasswordless.timePeriodInSeconds=60&__cb_tenant.rateLimitConfiguration.sendRegistrationVerification.enabled=false&tenant.rateLimitConfiguration.sendRegistrationVerification.limit=5&tenant.rateLimitConfiguration.sendRegistrationVerification.timePeriodInSeconds=60&__cb_tenant.rateLimitConfiguration.sendTwoFactor.enabled=false&tenant.rateLimitConfiguration.sendTwoFactor.limit=5&tenant.rateLimitConfiguration.sendTwoFactor.timePeriodInSeconds=60".getBytes(StandardCharsets.UTF_8));
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(uri)
+ .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8")
+ .header("Accept-Encoding", "gzip, deflate, br")
+ .header("Accept-Language", "en-US,en;q=0.5")
+ .header("Cache-Control", "max-age=0")
+ .header("Connection", "keep-alive")
+ .header("Content-Type", "application/x-www-form-urlencoded")
+ .header("Cookie", "_pk_id.1.8f36=19a58ac7d2eae34e.1703797254.; _pk_ses.1.8f36=1; fusionauth.locale=en; fusionauth.timezone=America/Denver; fusionauth.trusted-device.LJ9DfxybqbRJDIEar0MjOs1Dh9t4CUII7Ynx6BwZTbM=vAZ_0ETfK-utHZr6ErKdZm3s13LnYSmo8v417oiOB2wYmD09Nb_EYTcZ0RZFfkFf; fusionauth.known-device.LJ9DfxybqbRJDIEar0MjOs1Dh9t4CUII7Ynx6BwZTbM=oR3hEmqnAZQNtsYHG-3iSfkTVaohg0AQx_4WTfIS_213Lz6hxqPrBoj5LePRFqQd; federated.csrf=_I0Ug4kFjA7XhWva; fusionauth.sso=AoG7EJ2m0K5rARXr8LtYE_mjHpZSI2gMuHSzl-LD3e9SyBTTQszseRICR14rbUiqz7cwDfI3FgNcWbJBjvge506EHUfwBw-zGaM6pkDVoXfDIrOJdNUuCa8Ypzlt9lA6EqlTXZVWucErgXU1GxzikDA; fusionauth.remember-device=QkJCARHvawOLQQo6u55SnnjQnB9azwdr-fk7WmSc7NEEokml; fusionauth.rt=532xIZvgP15j7_s6XwKSh33Fj10waD8GeRqE7nEQe4D4CZl2VSitmQ; fusionauth.at=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCIsImd0eSI6WyJhdXRob3JpemF0aW9uX2NvZGUiXSwia2lkIjoiY2QwYWIwYmUzIn0.eyJhdWQiOiIzYzIxOWU1OC1lZDBlLTRiMTgtYWQ0OC1mNGY5Mjc5M2FlMzIiLCJleHAiOjE3MDU3MDI2NzksImlhdCI6MTcwNTcwMjYxOSwiaXNzIjoiYWNtZS5jb20iLCJzdWIiOiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDEiLCJqdGkiOiIwNzI5YjczMy03OGQ1LTQwNDAtOTY5Ni0zNWVjYWQ5YWYyZDgiLCJhdXRoZW50aWNhdGlvblR5cGUiOiJQSU5HIiwiZW1haWwiOiJhZG1pbkBmdXNpb25hdXRoLmlvIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImFwcGxpY2F0aW9uSWQiOiIzYzIxOWU1OC1lZDBlLTRiMTgtYWQ0OC1mNGY5Mjc5M2FlMzIiLCJzY29wZSI6Im9mZmxpbmVfYWNjZXNzIiwicm9sZXMiOlsiYWRtaW4iXSwic2lkIjoiODRhNGZmYzMtMWMzYy00OTA3LWIwZTgtZmY0NWNmNDFjMTkxIiwiYXV0aF90aW1lIjoxNzA1NzAyNjE5LCJ0aWQiOiIzMDY2MzEzMi02NDY0LTY2NjUtMzAzMi0zMjY0NjY2MTM5MzQifQ.twCgoWKPVLJuBaTCGTMdKWto8XICHpCk6zp2QkSIjNM; io.fusionauth.app.action.admin.tenant.IndexAction$s=QkJCA9x7yVuN7noZSN59WZMzJZGPcjSGAJjVr5ghJbPz3sOOcoCAS7PrcyMXBax0Cb6JdqCebWfX0tD4Y3lt5EK9KM8=")
+ .header("Origin", "https://local.fusionauth.io:9013")
+ .header("Referer", "https://local.fusionauth.io:9013/")
+ .header("Sec-Fetch-Dest", "document")
+ .header("Sec-Fetch-Mode", "navigate")
+ .header("Sec-Fetch-Site", "same-origin")
+ .header("Sec-Fetch-User", "?1")
+ .header("Sec-GPC", "1")
+ .header("Upgrade-Insecure-Requests", "1")
+ .header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
+ .header("sec-ch-ua", "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Brave\";v=\"120\"")
+ .header("sec-ch-ua-mobile", "?0")
+ .header("sec-ch-ua-platform", "\"macOS\"")
+ .POST(body)
+ .build();
+
+ var response = client.send(request, r -> BodySubscribers.ofInputStream());
+ assertEquals(response.statusCode(), 200);
+ }
+ }
+
@Test(dataProvider = "schemes")
public void unicode(String scheme) throws Exception {
HTTPHandler handler = (req, res) -> {
diff --git a/src/test/java/io/fusionauth/http/ExpectTest.java b/src/test/java/io/fusionauth/http/ExpectTest.java
index a573bc6..a069318 100644
--- a/src/test/java/io/fusionauth/http/ExpectTest.java
+++ b/src/test/java/io/fusionauth/http/ExpectTest.java
@@ -44,11 +44,6 @@ public class ExpectTest extends BaseTest {
public static final String RequestBody = "{\"message\":\"Hello World\"";
- static {
- System.setProperty("sun.net.http.retryPost", "false");
- System.setProperty("jdk.httpclient.allowRestrictedHeaders", "connection");
- }
-
@Test(dataProvider = "schemes")
public void expect(String scheme) throws Exception {
HTTPHandler handler = (req, res) -> {