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) -> {