Merge remote-tracking branch 'origin/jetty-12.0.x' into jetty-12.0.x-8606-scopeListeners

This commit is contained in:
Lachlan Roberts 2022-09-28 14:43:24 +10:00
commit b893186f5e
138 changed files with 2600 additions and 1519 deletions

View File

@ -19,10 +19,12 @@ This release process will produce releases:
- [ ] Link this issue to the target [GitHub Project(s)](https://github.com/eclipse/jetty.project/projects).
- [ ] Assign this issue to a "release manager".
- [ ] Review [draft security advisories](https://github.com/eclipse/jetty.project/security/advisories). Ensure that issues are created and assigned to GitHub Projects to capture any advisories that will be announced.
- [ ] Create the [GitHub Project(s)](https://github.com/eclipse/jetty.project/projects) for the next releases.
- [ ] Review dependabot status. If there has not been a recent run, run [manually](https://github.com/eclipse/jetty.project/network/updates) and review resulting PRs for inclusion.
- [ ] Review the issues/PRs assigned to the target [GitHub Project(s)](https://github.com/eclipse/jetty.project/projects). Any PRs that are moved to next releases should be commented on so their authors are informed.
- [ ] Freeze the target [GitHub Project(s)](https://github.com/eclipse/jetty.project/projects) by editing their names to "Jetty X.Y.Z FROZEN"
- [ ] Update [GitHub Project(s)](https://github.com/eclipse/jetty.project/projects)
+ [ ] Create new project for the next releases (not this release).
+ [ ] Ensure new project is public (not private)
+ [ ] Freeze the target [GitHub Project(s)](https://github.com/eclipse/jetty.project/projects) by editing their names to "Jetty X.Y.Z FROZEN"
+ [ ] Review the issues/PRs assigned to the target [GitHub Project(s)](https://github.com/eclipse/jetty.project/projects). Any tasks that are not-yet-started are moved to next releases.
- [ ] Review dependabot status. [Manually](https://github.com/eclipse/jetty.project/network/updates) run dependabot if needed and review resulting PRs for inclusion.
- [ ] Wait 24 hours from last change to the issues/PRs included in FROZEN GitHub Project(s).
- [ ] Verify target [project(s)](https://github.com/eclipse/jetty.project/projects) are complete.
- [ ] Verify that branch `jetty-10.0.x` is merged to branch `jetty-11.0.x`.
@ -35,6 +37,9 @@ This release process will produce releases:
+ [ ] Push release branches `release/<ver>` to to https://github.com/eclipse/jetty.project
+ [ ] Push release tags `jetty-<ver>` to https://github.com/eclipse/jetty.project
+ [ ] Edit a draft release (for each Jetty release) in GitHub (https://github.com/eclipse/jetty.project/releases). Content is generated with the "changelog tool".
Be mindful of the order you create multiple release drafts. The first one created will be the "oldest" when published. (eg: Draft is 9, then 10, then 11)
The last created "draft" will show up as "latest" in the github UI.
If you have to reroll, you'll have to delete the drafts and recreate them (especially so if 9 w/timestamp is in the mix of releases being worked on)
- [ ] Assign issue to "test manager", who will oversee the testing of the staged releases.
+ [ ] Test [CometD](https://github.com/cometd/cometd).
+ [ ] Test [Reactive HttpClient](https://github.com/jetty-project/jetty-reactive-httpclient).
@ -53,7 +58,7 @@ This release process will produce releases:
- [ ] Update Jetty versions on the web sites.
+ [ ] Update (or check) [Download](https://www.eclipse.org/jetty/download.php) page is updated.
+ [ ] Update (or check) documentation page(s) are updated.
- [ ] Publish GitHub Releases.
- [ ] Publish GitHub Releases in the order of oldest (eg: 9) to newest (eg: 11) (to ensure that "latest" in github is truly the latest)
- [ ] Prepare release announcement for mailing lists.
- [ ] Publish any [security advisories](https://github.com/eclipse/jetty.project/security/advisories).
+ [ ] Edit `VERSION.txt` to include any actual CVE number next to correspondent issue.

View File

@ -81,6 +81,8 @@ public class ContinueProtocolHandler implements ProtocolHandler
conversation.updateResponseListeners(null);
HttpExchange exchange = conversation.getExchanges().peekLast();
assert exchange != null;
if (response.getStatus() == HttpStatus.CONTINUE_100)
{
// All good, continue.

View File

@ -0,0 +1,96 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.client;
import java.util.List;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpStatus;
/**
* <p>A protocol handler that handles the 103 response code.</p>
*/
public class EarlyHintsProtocolHandler implements ProtocolHandler
{
public static final String NAME = "early-hints";
@Override
public String getName()
{
return NAME;
}
@Override
public boolean accept(Request request, Response response)
{
return response.getStatus() == HttpStatus.EARLY_HINT_103;
}
@Override
public Response.Listener getResponseListener()
{
return new EarlyHintsListener();
}
protected void onEarlyHints(Request request, HttpFields responseHeaders)
{
}
private class EarlyHintsListener extends BufferingResponseListener
{
private final ResponseNotifier notifier = new ResponseNotifier();
@Override
public void onSuccess(Response response)
{
Request request = response.getRequest();
HttpConversation conversation = ((HttpRequest)request).getConversation();
// Reset the conversation listeners, since we are going to receive another response code.
conversation.updateResponseListeners(null);
HttpExchange exchange = conversation.getExchanges().peekLast();
assert exchange != null;
HttpFields responseHeaders = HttpFields.build(response.getHeaders());
exchange.resetResponse();
onEarlyHints(request, responseHeaders);
}
@Override
public void onFailure(Response response, Throwable failure)
{
HttpConversation conversation = ((HttpRequest)response.getRequest()).getConversation();
// Reset the conversation listeners to allow the conversation to be completed.
conversation.updateResponseListeners(null);
HttpExchange exchange = conversation.getExchanges().peekLast();
if (exchange != null)
{
List<Response.ResponseListener> listeners = exchange.getResponseListeners();
HttpContentResponse contentResponse = new HttpContentResponse(response, getContent(), getMediaType(), getEncoding());
notifier.forwardFailureComplete(listeners, exchange.getRequest(), exchange.getRequestFailure(), contentResponse, failure);
}
}
@Override
public void onComplete(Result result)
{
}
}
}

View File

@ -221,6 +221,8 @@ public class HttpClient extends ContainerLifeCycle
setSocketAddressResolver(new SocketAddressResolver.Async(getExecutor(), getScheduler(), getAddressResolutionTimeout()));
handlers.put(new ContinueProtocolHandler());
handlers.put(new ProcessingProtocolHandler());
handlers.put(new EarlyHintsProtocolHandler());
handlers.put(new RedirectProtocolHandler(this));
handlers.put(new WWWAuthenticationProtocolHandler(this));
handlers.put(new ProxyAuthenticationProtocolHandler(this));

View File

@ -326,6 +326,10 @@ public abstract class HttpSender
public void proceed(HttpExchange exchange, Throwable failure)
{
// Received a 100 Continue, although Expect header was not sent.
if (!contentSender.expect100)
return;
contentSender.expect100 = false;
if (failure == null)
contentSender.iterate();

View File

@ -0,0 +1,96 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.client;
import java.util.List;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpStatus;
/**
* <p>A protocol handler that handles the 102 response code.</p>
*/
public class ProcessingProtocolHandler implements ProtocolHandler
{
public static final String NAME = "processing";
@Override
public String getName()
{
return NAME;
}
@Override
public boolean accept(Request request, Response response)
{
return response.getStatus() == HttpStatus.PROCESSING_102;
}
@Override
public Response.Listener getResponseListener()
{
return new ProcessingListener();
}
protected void onProcessing(Request request, HttpFields responseHeaders)
{
}
private class ProcessingListener extends BufferingResponseListener
{
private final ResponseNotifier notifier = new ResponseNotifier();
@Override
public void onSuccess(Response response)
{
Request request = response.getRequest();
HttpConversation conversation = ((HttpRequest)request).getConversation();
// Reset the conversation listeners, since we are going to receive another response code.
conversation.updateResponseListeners(null);
HttpExchange exchange = conversation.getExchanges().peekLast();
assert exchange != null;
HttpFields responseHeaders = HttpFields.build(response.getHeaders());
exchange.resetResponse();
onProcessing(request, responseHeaders);
}
@Override
public void onFailure(Response response, Throwable failure)
{
HttpConversation conversation = ((HttpRequest)response.getRequest()).getConversation();
// Reset the conversation listeners to allow the conversation to be completed.
conversation.updateResponseListeners(null);
HttpExchange exchange = conversation.getExchanges().peekLast();
if (exchange != null)
{
List<Response.ResponseListener> listeners = exchange.getResponseListeners();
HttpContentResponse contentResponse = new HttpContentResponse(response, getContent(), getMediaType(), getEncoding());
notifier.forwardFailureComplete(listeners, exchange.getRequest(), exchange.getRequestFailure(), contentResponse, failure);
}
}
@Override
public void onComplete(Result result)
{
}
}
}

View File

@ -37,6 +37,7 @@ import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -74,7 +75,7 @@ public class InputStreamResponseListener extends Listener.Adapter
private static final Logger LOG = LoggerFactory.getLogger(InputStreamResponseListener.class);
private static final Chunk EOF = new Chunk(BufferUtil.EMPTY_BUFFER, Callback.NOOP);
private final Object lock = this;
private final AutoLock.WithCondition lock = new AutoLock.WithCondition();
private final CountDownLatch responseLatch = new CountDownLatch(1);
private final CountDownLatch resultLatch = new CountDownLatch(1);
private final AtomicReference<InputStream> stream = new AtomicReference<>();
@ -91,7 +92,7 @@ public class InputStreamResponseListener extends Listener.Adapter
@Override
public void onHeaders(Response response)
{
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
this.response = response;
responseLatch.countDown();
@ -110,7 +111,7 @@ public class InputStreamResponseListener extends Listener.Adapter
}
boolean closed;
synchronized (lock)
try (AutoLock.WithCondition l = lock.lock())
{
closed = this.closed;
if (!closed)
@ -118,7 +119,7 @@ public class InputStreamResponseListener extends Listener.Adapter
if (LOG.isDebugEnabled())
LOG.debug("Queueing content {}", content);
chunks.add(new Chunk(content, callback));
lock.notifyAll();
l.signalAll();
}
}
@ -133,11 +134,11 @@ public class InputStreamResponseListener extends Listener.Adapter
@Override
public void onSuccess(Response response)
{
synchronized (lock)
try (AutoLock.WithCondition l = lock.lock())
{
if (!closed)
chunks.add(EOF);
lock.notifyAll();
l.signalAll();
}
if (LOG.isDebugEnabled())
@ -148,13 +149,13 @@ public class InputStreamResponseListener extends Listener.Adapter
public void onFailure(Response response, Throwable failure)
{
List<Callback> callbacks;
synchronized (lock)
try (AutoLock.WithCondition l = lock.lock())
{
if (this.failure != null)
return;
this.failure = failure;
callbacks = drain();
lock.notifyAll();
l.signalAll();
}
if (LOG.isDebugEnabled())
@ -168,7 +169,7 @@ public class InputStreamResponseListener extends Listener.Adapter
{
Throwable failure = result.getFailure();
List<Callback> callbacks = Collections.emptyList();
synchronized (lock)
try (AutoLock.WithCondition l = lock.lock())
{
this.result = result;
if (result.isFailed() && this.failure == null)
@ -179,7 +180,7 @@ public class InputStreamResponseListener extends Listener.Adapter
// Notify the response latch in case of request failures.
responseLatch.countDown();
resultLatch.countDown();
lock.notifyAll();
l.signalAll();
}
if (LOG.isDebugEnabled())
@ -211,7 +212,7 @@ public class InputStreamResponseListener extends Listener.Adapter
boolean expired = !responseLatch.await(timeout, unit);
if (expired)
throw new TimeoutException();
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
// If the request failed there is no response.
if (response == null)
@ -237,7 +238,7 @@ public class InputStreamResponseListener extends Listener.Adapter
boolean expired = !resultLatch.await(timeout, unit);
if (expired)
throw new TimeoutException();
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
return result;
}
@ -261,7 +262,7 @@ public class InputStreamResponseListener extends Listener.Adapter
private List<Callback> drain()
{
List<Callback> callbacks = new ArrayList<>();
synchronized (lock)
try (AutoLock ignored = lock.lock())
{
while (true)
{
@ -294,7 +295,7 @@ public class InputStreamResponseListener extends Listener.Adapter
{
int result;
Callback callback = null;
synchronized (lock)
try (AutoLock.WithCondition l = lock.lock())
{
Chunk chunk;
while (true)
@ -312,7 +313,7 @@ public class InputStreamResponseListener extends Listener.Adapter
if (closed)
throw new AsynchronousCloseException();
lock.wait();
l.await();
}
ByteBuffer buffer = chunk.buffer;
@ -346,13 +347,13 @@ public class InputStreamResponseListener extends Listener.Adapter
public void close() throws IOException
{
List<Callback> callbacks;
synchronized (lock)
try (AutoLock.WithCondition l = lock.lock())
{
if (closed)
return;
closed = true;
callbacks = drain();
lock.notifyAll();
l.signalAll();
}
if (LOG.isDebugEnabled())

View File

@ -103,7 +103,7 @@ public class HttpTester
return new Input(data.slice())
{
@Override
public int fillBuffer() throws IOException
public int fillBuffer()
{
_eof = true;
return -1;
@ -162,10 +162,7 @@ public class HttpTester
public static Request parseRequest(String request)
{
Request r = new Request();
HttpParser parser = new HttpParser(r);
parser.parseNext(BufferUtil.toBuffer(request));
return r;
return parseRequest(BufferUtil.toBuffer(request));
}
public static Request parseRequest(ByteBuffer request)
@ -176,6 +173,35 @@ public class HttpTester
return r;
}
public static Request parseRequest(InputStream requestStream) throws IOException
{
Request r = new Request();
HttpParser parser = new HttpParser(r);
return parseMessage(requestStream, parser) ? r : null;
}
private static boolean parseMessage(InputStream stream, HttpParser parser) throws IOException
{
// Read and parse a character at a time so we never can read more than we should.
byte[] array = new byte[1];
ByteBuffer buffer = ByteBuffer.wrap(array);
while (true)
{
buffer.position(1);
int read = stream.read(array);
if (read < 0)
parser.atEOF();
else
buffer.position(0);
if (parser.parseNext(buffer))
return true;
else if (read < 0)
return false;
}
}
public static Response parseResponse(String response)
{
Response r = new Response();
@ -196,26 +222,7 @@ public class HttpTester
{
Response r = new Response();
HttpParser parser = new HttpParser(r);
// Read and parse a character at a time so we never can read more than we should.
byte[] array = new byte[1];
ByteBuffer buffer = ByteBuffer.wrap(array);
buffer.limit(1);
while (true)
{
buffer.position(1);
int l = responseStream.read(array);
if (l < 0)
parser.atEOF();
else
buffer.position(0);
if (parser.parseNext(buffer))
return r;
else if (l < 0)
return null;
}
return parseMessage(responseStream, parser) ? r : null;
}
public static Response parseResponse(Input in) throws IOException
@ -406,12 +413,12 @@ public class HttpTester
try
{
_content.write(BufferUtil.toArray(ref));
return false;
}
catch (IOException e)
{
throw new RuntimeException(e);
}
return false;
}
@Override

View File

@ -244,9 +244,8 @@ public class MultiPartCaptureTest
List<MultiPart.Part> charSetParts = allParts.get("_charset_");
if (charSetParts != null)
{
Promise.Completable<String> promise = new Promise.Completable<>();
Content.Source.asString(charSetParts.get(0).getContent(), StandardCharsets.US_ASCII, promise);
defaultCharset = promise.get();
defaultCharset = Promise.Completable.<String>with(p -> Content.Source.asString(charSetParts.get(0).getContent(), StandardCharsets.US_ASCII, p))
.get();
}
for (NameValue expected : partContainsContents)

View File

@ -383,9 +383,7 @@ public class HTTP2Client extends ContainerLifeCycle
public CompletableFuture<Session> connect(SslContextFactory sslContextFactory, SocketAddress address, Session.Listener listener)
{
Promise.Completable<Session> result = new Promise.Completable<>();
connect(sslContextFactory, address, listener, result);
return result;
return Promise.Completable.with(p -> connect(sslContextFactory, address, listener, p));
}
public void connect(SslContextFactory sslContextFactory, SocketAddress address, Session.Listener listener, Promise<Session> promise)

View File

@ -60,9 +60,7 @@ public interface Session
*/
public default CompletableFuture<Stream> newStream(HeadersFrame frame, Stream.Listener listener)
{
Promise.Completable<Stream> result = new Promise.Completable<>();
newStream(frame, result, listener);
return result;
return Promise.Completable.with(p -> newStream(frame, p, listener));
}
/**
@ -87,6 +85,17 @@ public interface Session
*/
public int priority(PriorityFrame frame, Callback callback);
/**
* <p>Sends the given SETTINGS {@code frame} to configure the session.</p>
*
* @param frame the SETTINGS frame to send
* @return a CompletableFuture that is notified when the frame has been sent
*/
public default CompletableFuture<Void> settings(SettingsFrame frame)
{
return Callback.Completable.with(c -> settings(frame, c));
}
/**
* <p>Sends the given SETTINGS {@code frame} to configure the session.</p>
*

View File

@ -62,9 +62,7 @@ public interface Stream
*/
public default CompletableFuture<Stream> headers(HeadersFrame frame)
{
Promise.Completable<Stream> result = new Promise.Completable<>();
headers(frame, Callback.from(() -> result.succeeded(this), result::failed));
return result;
return Promise.Completable.with(p -> headers(frame, Callback.from(() -> p.succeeded(this), p::failed)));
}
/**
@ -85,9 +83,7 @@ public interface Stream
*/
public default CompletableFuture<Stream> push(PushPromiseFrame frame, Listener listener)
{
Promise.Completable<Stream> result = new Promise.Completable<>();
push(frame, result, listener);
return result;
return Promise.Completable.with(p -> push(frame, p, listener));
}
/**
@ -139,9 +135,7 @@ public interface Stream
*/
public default CompletableFuture<Stream> data(DataFrame frame)
{
Promise.Completable<Stream> result = new Promise.Completable<>();
data(frame, Callback.from(() -> result.succeeded(this), result::failed));
return result;
return Promise.Completable.with(p -> data(frame, Callback.from(() -> p.succeeded(this), p::failed)));
}
/**
@ -155,7 +149,18 @@ public interface Stream
/**
* <p>Sends the given RST_STREAM {@code frame}.</p>
*
* @param frame the RST_FRAME to send
* @param frame the RST_STREAM frame to send
* @return the CompletableFuture that gets notified when the frame has been sent
*/
public default CompletableFuture<Void> reset(ResetFrame frame)
{
return Callback.Completable.with(c -> reset(frame, c));
}
/**
* <p>Sends the given RST_STREAM {@code frame}.</p>
*
* @param frame the RST_STREAM frame to send
* @param callback the callback that gets notified when the frame has been sent
*/
public void reset(ResetFrame frame, Callback callback);
@ -348,7 +353,7 @@ public interface Stream
* <p>Callback method invoked when a RST_STREAM frame has been received for this stream.</p>
*
* @param stream the stream
* @param frame the RST_FRAME received
* @param frame the RST_STREAM frame received
* @param callback the callback to complete when the reset has been handled
*/
public default void onReset(Stream stream, ResetFrame frame, Callback callback)
@ -415,18 +420,6 @@ public interface Stream
return frame;
}
@Override
public void retain()
{
throw new UnsupportedOperationException();
}
@Override
public boolean release()
{
return true;
}
@Override
public String toString()
{
@ -439,6 +432,18 @@ public interface Stream
{
super(new DataFrame(streamId, BufferUtil.EMPTY_BUFFER, true));
}
@Override
public void retain()
{
throw new UnsupportedOperationException();
}
@Override
public boolean release()
{
return true;
}
}
}
}

View File

@ -19,6 +19,9 @@ import java.util.function.Supplier;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
@ -53,10 +56,12 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
private final HttpChannel _httpChannel;
private final HTTP2Stream _stream;
private final long _nanoTime;
private MetaData.Request _requestMetaData;
private MetaData.Response _responseMetaData;
private Content.Chunk _chunk;
private MetaData.Response _metaData;
private boolean committed;
private boolean _demand;
private boolean _expects100Continue;
public HttpStreamOverHTTP2(HTTP2ServerConnection connection, HttpChannel httpChannel, HTTP2Stream stream)
{
@ -82,9 +87,9 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
{
try
{
MetaData.Request request = (MetaData.Request)frame.getMetaData();
_requestMetaData = (MetaData.Request)frame.getMetaData();
Runnable handler = _httpChannel.onRequest(request);
Runnable handler = _httpChannel.onRequest(_requestMetaData);
if (frame.isEndStream())
{
@ -94,16 +99,15 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
}
}
HttpFields fields = request.getFields();
HttpFields fields = _requestMetaData.getFields();
// TODO: handle 100 continue.
// _expect100Continue = fields.contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString());
_expects100Continue = fields.contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString());
if (LOG.isDebugEnabled())
{
LOG.debug("HTTP2 request #{}/{}, {} {} {}{}{}",
_stream.getId(), Integer.toHexString(_stream.getSession().hashCode()),
request.getMethod(), request.getURI(), request.getHttpVersion(),
_requestMetaData.getMethod(), _requestMetaData.getURI(), _requestMetaData.getHttpVersion(),
System.lineSeparator(), fields);
}
@ -147,6 +151,12 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
chunk = createChunk(data);
data.release();
// Some content is read, but the 100 Continue interim
// response has not been sent yet, then don't bother
// sending it later, as the client already sent the content.
if (_expects100Continue && chunk.hasRemaining())
_expects100Continue = false;
try (AutoLock ignored = lock.lock())
{
_chunk = chunk;
@ -164,7 +174,6 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
// We may have a non-demanded chunk in case of trailers.
if (_chunk != null)
notify = true;
// Make sure we only demand(1) to make this method is idempotent.
else if (!_demand)
demand = _demand = true;
}
@ -176,6 +185,11 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
}
else if (demand)
{
if (_expects100Continue)
{
_expects100Continue = false;
send(_requestMetaData, HttpGenerator.CONTINUE_100_INFO, false, null, Callback.NOOP);
}
_stream.demand();
}
}
@ -220,9 +234,12 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
private Content.Chunk createChunk(Stream.Data data)
{
// As we are passing the ByteBuffer to the Chunk we need to retain.
data.retain();
DataFrame frame = data.frame();
if (frame.isEndStream() && frame.remaining() == 0)
return Content.Chunk.EOF;
// We need to retain because we are passing the ByteBuffer to the Chunk.
data.retain();
return Content.Chunk.from(frame.getData(), frame.isEndStream(), data);
}
@ -244,7 +261,7 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
private void sendHeaders(MetaData.Request request, MetaData.Response response, ByteBuffer content, boolean last, Callback callback)
{
_metaData = response;
_responseMetaData = response;
HeadersFrame headersFrame;
DataFrame dataFrame = null;
@ -256,12 +273,17 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
if (HttpStatus.isInterim(response.getStatus()))
{
// Must not commit interim responses.
if (hasContent)
{
callback.failed(new IllegalStateException("Interim response cannot have content"));
return;
}
headersFrame = new HeadersFrame(streamId, _metaData, null, false);
if (_expects100Continue && response.getStatus() == HttpStatus.CONTINUE_100)
_expects100Continue = false;
headersFrame = new HeadersFrame(streamId, response, null, false);
}
else
{
@ -272,7 +294,7 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
long contentLength = response.getContentLength();
if (contentLength < 0)
{
_metaData = new MetaData.Response(
_responseMetaData = new MetaData.Response(
response.getHttpVersion(),
response.getStatus(),
response.getReason(),
@ -290,7 +312,7 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
if (hasContent)
{
headersFrame = new HeadersFrame(streamId, _metaData, null, false);
headersFrame = new HeadersFrame(streamId, response, null, false);
if (last)
{
HttpFields trailers = retrieveTrailers();
@ -313,27 +335,27 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
{
if (last)
{
if (isTunnel(request, _metaData))
if (isTunnel(request, response))
{
headersFrame = new HeadersFrame(streamId, _metaData, null, false);
headersFrame = new HeadersFrame(streamId, response, null, false);
}
else
{
HttpFields trailers = retrieveTrailers();
if (trailers == null)
{
headersFrame = new HeadersFrame(streamId, _metaData, null, true);
headersFrame = new HeadersFrame(streamId, response, null, true);
}
else
{
headersFrame = new HeadersFrame(streamId, _metaData, null, false);
headersFrame = new HeadersFrame(streamId, response, null, false);
trailersFrame = new HeadersFrame(streamId, new MetaData(HttpVersion.HTTP_2, trailers), null, true);
}
}
}
else
{
headersFrame = new HeadersFrame(streamId, _metaData, null, false);
headersFrame = new HeadersFrame(streamId, response, null, false);
}
}
}
@ -342,9 +364,10 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
{
LOG.debug("HTTP2 Response #{}/{}:{}{} {}{}{}",
_stream.getId(), Integer.toHexString(_stream.getSession().hashCode()),
System.lineSeparator(), HttpVersion.HTTP_2, _metaData.getStatus(),
System.lineSeparator(), _metaData.getFields());
System.lineSeparator(), HttpVersion.HTTP_2, response.getStatus(),
System.lineSeparator(), response.getFields());
}
_stream.send(new HTTP2Stream.FrameList(headersFrame, dataFrame, trailersFrame), callback);
}
@ -352,7 +375,7 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
{
boolean isHeadRequest = HttpMethod.HEAD.is(request.getMethod());
boolean hasContent = BufferUtil.hasContent(content) && !isHeadRequest;
if (hasContent || (last && !isTunnel(request, _metaData)))
if (hasContent || (last && !isTunnel(request, _responseMetaData)))
{
if (last)
{
@ -387,7 +410,7 @@ public class HttpStreamOverHTTP2 implements HttpStream, HTTP2Channel.Server
private HttpFields retrieveTrailers()
{
Supplier<HttpFields> supplier = _metaData.getTrailersSupplier();
Supplier<HttpFields> supplier = _responseMetaData.getTrailersSupplier();
if (supplier == null)
return null;
HttpFields trailers = supplier.get();

View File

@ -289,14 +289,13 @@ public abstract class FlowControlStrategyTest
Stream stream = promise.get(5, TimeUnit.SECONDS);
// Send first chunk that exceeds the window.
Callback.Completable completable = new Callback.Completable();
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(size * 2), false), completable);
CompletableFuture<Stream> completable = stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(size * 2), false));
settingsLatch.await(5, TimeUnit.SECONDS);
completable.thenRun(() ->
completable.thenAccept(s ->
{
// Send the second chunk of data, must not arrive since we're flow control stalled on the client.
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(size * 2), true), Callback.NOOP);
s.data(new DataFrame(s.getId(), ByteBuffer.allocate(size * 2), true));
});
assertFalse(dataLatch.await(1, TimeUnit.SECONDS));
@ -341,9 +340,8 @@ public abstract class FlowControlStrategyTest
Map<Integer, Integer> settings = new HashMap<>();
settings.put(SettingsFrame.INITIAL_WINDOW_SIZE, windowSize);
Callback.Completable completable = new Callback.Completable();
session.settings(new SettingsFrame(settings, false), completable);
completable.thenRun(settingsLatch::countDown);
session.settings(new SettingsFrame(settings, false))
.thenRun(settingsLatch::countDown);
assertTrue(settingsLatch.await(5, TimeUnit.SECONDS));
@ -641,13 +639,8 @@ public abstract class FlowControlStrategyTest
{
MetaData.Response metaData = new MetaData.Response(HttpVersion.HTTP_2, 200, HttpFields.EMPTY);
HeadersFrame responseFrame = new HeadersFrame(stream.getId(), metaData, null, false);
Callback.Completable completable = new Callback.Completable();
stream.headers(responseFrame, completable);
completable.thenRun(() ->
{
DataFrame dataFrame = new DataFrame(stream.getId(), ByteBuffer.wrap(data), true);
stream.data(dataFrame, Callback.NOOP);
});
stream.headers(responseFrame)
.thenAccept(s -> s.data(new DataFrame(s.getId(), ByteBuffer.wrap(data), true)));
return null;
}
});
@ -691,8 +684,7 @@ public abstract class FlowControlStrategyTest
{
MetaData metaData = new MetaData.Response(HttpVersion.HTTP_2, 200, HttpFields.EMPTY);
HeadersFrame responseFrame = new HeadersFrame(stream.getId(), metaData, null, false);
Callback.Completable completable = new Callback.Completable();
stream.headers(responseFrame, completable);
CompletableFuture<Stream> completable = stream.headers(responseFrame);
stream.demand();
return new Stream.Listener()
{
@ -700,12 +692,13 @@ public abstract class FlowControlStrategyTest
public void onDataAvailable(Stream stream)
{
Stream.Data data = stream.readData();
completable.thenRun(() -> stream.data(data.frame(), Callback.from(() ->
{
data.release();
if (!data.frame().isEndStream())
stream.demand();
})));
completable.thenAccept(s -> s.data(data.frame())
.whenComplete((r, x) ->
{
data.release();
if (!data.frame().isEndStream())
stream.demand();
}));
}
};
}
@ -730,9 +723,8 @@ public abstract class FlowControlStrategyTest
ByteBuffer responseContent = ByteBuffer.wrap(responseData);
MetaData.Request metaData = newRequest("GET", HttpFields.EMPTY);
HeadersFrame requestFrame = new HeadersFrame(metaData, null, false);
Promise.Completable<Stream> completable = new Promise.Completable<>();
CountDownLatch latch = new CountDownLatch(1);
session.newStream(requestFrame, completable, new Stream.Listener()
session.newStream(requestFrame, new Stream.Listener()
{
@Override
public void onDataAvailable(Stream stream)
@ -745,12 +737,11 @@ public abstract class FlowControlStrategyTest
else
stream.demand();
}
});
completable.thenAccept(stream ->
})
.thenAccept(s ->
{
ByteBuffer requestContent = ByteBuffer.wrap(requestData);
DataFrame dataFrame = new DataFrame(stream.getId(), requestContent, true);
stream.data(dataFrame, Callback.NOOP);
s.data(new DataFrame(s.getId(), requestContent, true));
});
assertTrue(latch.await(5, TimeUnit.SECONDS));

View File

@ -222,8 +222,7 @@ public class HTTP2Test extends AbstractTest
CountDownLatch latch = new CountDownLatch(1);
MetaData.Request metaData = newRequest("POST", HttpFields.EMPTY);
HeadersFrame frame = new HeadersFrame(metaData, null, false);
Promise.Completable<Stream> streamCompletable = new Promise.Completable<>();
session.newStream(frame, streamCompletable, new Stream.Listener()
session.newStream(frame, new Stream.Listener()
{
@Override
public void onDataAvailable(Stream stream)
@ -235,18 +234,9 @@ public class HTTP2Test extends AbstractTest
else
stream.demand();
}
});
streamCompletable.thenCompose(stream ->
{
DataFrame dataFrame = new DataFrame(stream.getId(), ByteBuffer.allocate(512), false);
Callback.Completable dataCompletable = new Callback.Completable();
stream.data(dataFrame, dataCompletable);
return dataCompletable.thenApply(y -> stream);
}).thenAccept(stream ->
{
DataFrame dataFrame = new DataFrame(stream.getId(), ByteBuffer.allocate(1024), true);
stream.data(dataFrame, Callback.NOOP);
});
})
.thenCompose(s -> s.data(new DataFrame(s.getId(), ByteBuffer.allocate(512), false)))
.thenAccept(s -> s.data(new DataFrame(s.getId(), ByteBuffer.allocate(1024), true)));
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@ -546,9 +536,8 @@ public class HTTP2Test extends AbstractTest
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
Callback.Completable completable = new Callback.Completable();
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, HttpFields.EMPTY);
stream.headers(new HeadersFrame(stream.getId(), response, null, false), completable);
CompletableFuture<Stream> completable = stream.headers(new HeadersFrame(stream.getId(), response, null, false));
stream.demand();
return new Stream.Listener()
{
@ -559,11 +548,8 @@ public class HTTP2Test extends AbstractTest
data.release();
if (data.frame().isEndStream())
{
completable.thenRun(() ->
{
DataFrame endFrame = new DataFrame(stream.getId(), BufferUtil.EMPTY_BUFFER, true);
stream.data(endFrame, Callback.NOOP);
});
completable.thenAccept(s ->
s.data(new DataFrame(s.getId(), BufferUtil.EMPTY_BUFFER, true)));
}
else
{
@ -578,9 +564,8 @@ public class HTTP2Test extends AbstractTest
MetaData.Request metaData = newRequest("GET", HttpFields.EMPTY);
HeadersFrame frame = new HeadersFrame(metaData, null, false);
Promise.Completable<Stream> completable = new Promise.Completable<>();
CountDownLatch completeLatch = new CountDownLatch(2);
session.newStream(frame, completable, new Stream.Listener()
Stream stream = session.newStream(frame, new Stream.Listener()
{
@Override
public void onDataAvailable(Stream stream)
@ -592,8 +577,7 @@ public class HTTP2Test extends AbstractTest
else
stream.demand();
}
});
Stream stream = completable.get(5, TimeUnit.SECONDS);
}).get(5, TimeUnit.SECONDS);
long sleep = 1000;
DataFrame data1 = new DataFrame(stream.getId(), ByteBuffer.allocate(1024), false)

View File

@ -222,15 +222,14 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
@Override
public void process(Request request, org.eclipse.jetty.server.Response response, Callback callback)
{
Callback.Completable completable = new Callback.Completable();
response.write(false, ByteBuffer.allocate(1), completable);
completable.whenComplete((r, x) ->
{
if (x != null)
callback.failed(x);
else
response.write(true, ByteBuffer.allocate(2), callback);
});
Callback.Completable.with(c -> response.write(false, ByteBuffer.allocate(1), c))
.whenComplete((r, x) ->
{
if (x != null)
callback.failed(x);
else
response.write(true, ByteBuffer.allocate(2), callback);
});
}
});
@ -638,12 +637,10 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
int streamId = stream.getId();
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.NO_CONTENT_204, HttpFields.EMPTY);
HeadersFrame responseFrame = new HeadersFrame(streamId, response, null, false);
Callback.Completable callback = new Callback.Completable();
stream.headers(responseFrame, callback);
callback.thenRun(() -> stream.data(new DataFrame(streamId, ByteBuffer.wrap(bytes), true), Callback.NOOP));
HeadersFrame responseFrame = new HeadersFrame(stream.getId(), response, null, false);
stream.headers(responseFrame)
.thenAccept(s -> s.data(new DataFrame(s.getId(), ByteBuffer.wrap(bytes), true)));
return null;
}
});
@ -671,12 +668,10 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
HttpFields fields = HttpFields.build()
.put(":method", "get");
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, fields, 0);
int streamId = stream.getId();
HeadersFrame responseFrame = new HeadersFrame(streamId, response, null, false);
Callback.Completable callback = new Callback.Completable();
stream.headers(responseFrame, callback);
HeadersFrame responseFrame = new HeadersFrame(stream.getId(), response, null, false);
byte[] bytes = "hello".getBytes(StandardCharsets.US_ASCII);
callback.thenRun(() -> stream.data(new DataFrame(streamId, ByteBuffer.wrap(bytes), true), Callback.NOOP));
stream.headers(responseFrame)
.thenAccept(s -> s.data(new DataFrame(s.getId(), ByteBuffer.wrap(bytes), true)));
return null;
}
});
@ -705,9 +700,8 @@ public class HttpClientTransportOverHTTP2Test extends AbstractTest
int streamId = stream.getId();
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, HttpFields.EMPTY);
HeadersFrame responseFrame = new HeadersFrame(streamId, response, null, false);
Callback.Completable callback = new Callback.Completable();
stream.headers(responseFrame, callback);
callback.thenRun(() -> stream.data(new DataFrame(streamId, ByteBuffer.wrap(new byte[bytes]), true), Callback.NOOP));
stream.headers(responseFrame)
.thenAccept(s -> s.data(new DataFrame(s.getId(), ByteBuffer.wrap(new byte[bytes]), true)));
return null;
}
});

View File

@ -559,20 +559,17 @@ public class IdleTimeoutTest extends AbstractTest
session.newStream(requestFrame, promise, null);
Stream stream = promise.get(5, TimeUnit.SECONDS);
Callback.Completable completable1 = new Callback.Completable();
sleep(idleTimeout / 2);
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1), false), completable1);
completable1.thenCompose(nil ->
{
Callback.Completable completable2 = new Callback.Completable();
sleep(idleTimeout / 2);
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1), false), completable2);
return completable2;
}).thenRun(() ->
{
sleep(idleTimeout / 2);
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1), true), Callback.NOOP);
});
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1), false))
.thenCompose(s ->
{
sleep(idleTimeout / 2);
return s.data(new DataFrame(s.getId(), ByteBuffer.allocate(1), false));
}).thenAccept(s ->
{
sleep(idleTimeout / 2);
s.data(new DataFrame(s.getId(), ByteBuffer.allocate(1), true));
});
assertFalse(resetLatch.await(1, TimeUnit.SECONDS));
}

View File

@ -37,7 +37,6 @@ import org.eclipse.jetty.http2.internal.ErrorCode;
import org.eclipse.jetty.http2.internal.HTTP2Session;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.Promise;
import org.junit.jupiter.api.Test;
@ -74,12 +73,7 @@ public class MaxPushedStreamsTest extends AbstractTest
// Push maxPushed resources...
IntStream.range(0, maxPushed)
.mapToObj(i -> new PushPromiseFrame(stream.getId(), 0, newRequest("GET", "/push_" + i, HttpFields.EMPTY)))
.map(pushFrame ->
{
Promise.Completable<Stream> promise = new Promise.Completable<>();
stream.push(pushFrame, promise, null);
return promise;
})
.map(pushFrame -> stream.push(pushFrame, null))
// ... wait for the pushed streams...
.reduce(result, (cfList, cfStream) -> cfList.thenCombine(cfStream, add),
(cfList1, cfList2) -> cfList1.thenCombine(cfList2, addAll))
@ -87,8 +81,7 @@ public class MaxPushedStreamsTest extends AbstractTest
.thenApply(streams ->
{
PushPromiseFrame extraPushFrame = new PushPromiseFrame(stream.getId(), 0, newRequest("GET", "/push_extra", HttpFields.EMPTY));
FuturePromise<Stream> extraPromise = new FuturePromise<>();
stream.push(extraPushFrame, extraPromise, new Stream.Listener()
stream.push(extraPushFrame, new Stream.Listener()
{
@Override
public void onReset(Stream stream, ResetFrame frame, Callback callback)
@ -104,13 +97,13 @@ public class MaxPushedStreamsTest extends AbstractTest
.thenAccept(streams -> streams.forEach(pushedStream ->
{
DataFrame data = new DataFrame(pushedStream.getId(), BufferUtil.EMPTY_BUFFER, true);
pushedStream.data(data, Callback.NOOP);
pushedStream.data(data);
}))
// ... then send the response.
.thenRun(() ->
{
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, HttpFields.EMPTY);
stream.headers(new HeadersFrame(stream.getId(), response, null, true), Callback.NOOP);
stream.headers(new HeadersFrame(stream.getId(), response, null, true));
});
return null;
}

View File

@ -26,6 +26,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@ -356,9 +357,8 @@ public class PrefaceTest extends AbstractTest
http2Client.start();
CountDownLatch failureLatch = new CountDownLatch(1);
Promise.Completable<Session> promise = new Promise.Completable<>();
InetSocketAddress address = new InetSocketAddress("localhost", server.getLocalPort());
http2Client.connect(address, new Session.Listener()
CompletableFuture<Session> promise = http2Client.connect(address, new Session.Listener()
{
@Override
public void onFailure(Session session, Throwable failure, Callback callback)
@ -366,7 +366,7 @@ public class PrefaceTest extends AbstractTest
failureLatch.countDown();
callback.succeeded();
}
}, promise);
});
try (Socket socket = server.accept())
{

View File

@ -167,27 +167,24 @@ public class RawHTTP2ProxyTest
LOGGER.debug("SERVER2 received {}", data);
data.release();
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, HttpStatus.OK_200, HttpFields.EMPTY);
Callback.Completable completable1 = new Callback.Completable();
HeadersFrame reply = new HeadersFrame(stream.getId(), response, null, false);
if (LOGGER.isDebugEnabled())
LOGGER.debug("SERVER2 sending {}", reply);
stream.headers(reply, completable1);
completable1.thenCompose(ignored ->
{
Callback.Completable completable2 = new Callback.Completable();
DataFrame dataFrame = new DataFrame(stream.getId(), buffer1.slice(), false);
if (LOGGER.isDebugEnabled())
LOGGER.debug("SERVER2 sending {}", dataFrame);
stream.data(dataFrame, completable2);
return completable2;
}).thenRun(() ->
{
MetaData trailer = new MetaData(HttpVersion.HTTP_2, HttpFields.EMPTY);
HeadersFrame end = new HeadersFrame(stream.getId(), trailer, null, true);
if (LOGGER.isDebugEnabled())
LOGGER.debug("SERVER2 sending {}", end);
stream.headers(end, Callback.NOOP);
});
stream.headers(reply)
.thenCompose(s ->
{
DataFrame dataFrame = new DataFrame(s.getId(), buffer1.slice(), false);
if (LOGGER.isDebugEnabled())
LOGGER.debug("SERVER2 sending {}", dataFrame);
return s.data(dataFrame);
}).thenAccept(s ->
{
MetaData trailer = new MetaData(HttpVersion.HTTP_2, HttpFields.EMPTY);
HeadersFrame end = new HeadersFrame(s.getId(), trailer, null, true);
if (LOGGER.isDebugEnabled())
LOGGER.debug("SERVER2 sending {}", end);
s.headers(end);
});
}
};
}

View File

@ -27,6 +27,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Exchanger;
import java.util.concurrent.TimeUnit;
@ -168,8 +169,7 @@ public class StreamResetTest extends AbstractTest
{
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, 200, HttpFields.EMPTY);
HeadersFrame responseFrame = new HeadersFrame(stream.getId(), response, null, false);
Callback.Completable completable = new Callback.Completable();
stream.headers(responseFrame, completable);
CompletableFuture<Stream> completable = stream.headers(responseFrame);
stream.demand();
return new Stream.Listener()
{
@ -178,15 +178,8 @@ public class StreamResetTest extends AbstractTest
{
Stream.Data data = stream.readData();
data.release();
completable.thenRun(() ->
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(16), true), new Callback()
{
@Override
public void succeeded()
{
serverDataLatch.countDown();
}
}));
completable.thenCompose(s -> s.data(new DataFrame(s.getId(), ByteBuffer.allocate(16), true)))
.thenRun(serverDataLatch::countDown);
}
@Override
@ -329,9 +322,8 @@ public class StreamResetTest extends AbstractTest
try
{
commitLatch.await(5, TimeUnit.SECONDS);
Callback.Completable completable = new Callback.Completable();
stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), completable);
completable.thenRun(resetLatch::countDown);
stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code))
.thenRun(resetLatch::countDown);
}
catch (InterruptedException x)
{
@ -416,9 +408,8 @@ public class StreamResetTest extends AbstractTest
try
{
commitLatch.await(5, TimeUnit.SECONDS);
Callback.Completable completable = new Callback.Completable();
stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code), completable);
completable.thenRun(resetLatch::countDown);
stream.reset(new ResetFrame(stream.getId(), ErrorCode.CANCEL_STREAM_ERROR.code))
.thenRun(resetLatch::countDown);
}
catch (InterruptedException x)
{
@ -1053,8 +1044,7 @@ public class StreamResetTest extends AbstractTest
{
MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, 200, HttpFields.EMPTY);
HeadersFrame responseFrame = new HeadersFrame(stream.getId(), response, null, false);
Callback.Completable completable = new Callback.Completable();
stream.headers(responseFrame, completable);
stream.headers(responseFrame, Callback.NOOP);
return null;
}
}, http2Factory);

View File

@ -17,6 +17,7 @@ import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@ -174,19 +175,18 @@ public class TrailersTest extends AbstractTest
Stream stream = streamPromise.get(5, TimeUnit.SECONDS);
// Send some data.
Callback.Completable callback = new Callback.Completable();
stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(16), false), callback);
CompletableFuture<Stream> completable = stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(16), false));
assertTrue(trailerLatch.await(555, TimeUnit.SECONDS));
assertTrue(trailerLatch.await(5, TimeUnit.SECONDS));
// Send the trailers.
callback.thenRun(() ->
completable.thenRun(() ->
{
HttpFields.Mutable trailerFields = HttpFields.build();
trailerFields.put("X-Trailer", "true");
MetaData trailers = new MetaData(HttpVersion.HTTP_2, trailerFields);
HeadersFrame trailerFrame = new HeadersFrame(stream.getId(), trailers, null, true);
stream.headers(trailerFrame, Callback.NOOP);
stream.headers(trailerFrame);
});
assertTrue(latch.await(5, TimeUnit.SECONDS));
@ -331,18 +331,21 @@ public class TrailersTest extends AbstractTest
session.newStream(requestFrame, promise, null);
Stream stream = promise.get(5, TimeUnit.SECONDS);
ByteBuffer data = ByteBuffer.wrap(StringUtil.getUtf8Bytes("hello"));
Callback.Completable completable = new Callback.Completable();
stream.data(new DataFrame(stream.getId(), data, false), completable);
CountDownLatch failureLatch = new CountDownLatch(1);
completable.thenRun(() ->
{
// Invalid trailer: cannot contain pseudo headers.
HttpFields.Mutable trailerFields = HttpFields.build();
trailerFields.put(HttpHeader.C_METHOD, "GET");
MetaData trailer = new MetaData(HttpVersion.HTTP_2, trailerFields);
HeadersFrame trailerFrame = new HeadersFrame(stream.getId(), trailer, null, true);
stream.headers(trailerFrame, Callback.from(Callback.NOOP::succeeded, x -> failureLatch.countDown()));
});
stream.data(new DataFrame(stream.getId(), data, false))
.thenAccept(s ->
{
// Invalid trailer: cannot contain pseudo headers.
HttpFields.Mutable trailerFields = HttpFields.build();
trailerFields.put(HttpHeader.C_METHOD, "GET");
MetaData trailer = new MetaData(HttpVersion.HTTP_2, trailerFields);
s.headers(new HeadersFrame(s.getId(), trailer, null, true))
.exceptionally(x ->
{
failureLatch.countDown();
return s;
});
});
assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
}
@ -383,19 +386,17 @@ public class TrailersTest extends AbstractTest
});
Stream stream = promise.get(5, TimeUnit.SECONDS);
ByteBuffer data = ByteBuffer.wrap(StringUtil.getUtf8Bytes("hello"));
Callback.Completable completable = new Callback.Completable();
stream.data(new DataFrame(stream.getId(), data, false), completable);
completable.thenRun(() ->
{
// Disable checks for invalid headers.
((HTTP2Session)session).getGenerator().setValidateHpackEncoding(false);
// Invalid trailer: cannot contain pseudo headers.
HttpFields.Mutable trailerFields = HttpFields.build();
trailerFields.put(HttpHeader.C_METHOD, "GET");
MetaData trailer = new MetaData(HttpVersion.HTTP_2, trailerFields);
HeadersFrame trailerFrame = new HeadersFrame(stream.getId(), trailer, null, true);
stream.headers(trailerFrame, Callback.NOOP);
});
stream.data(new DataFrame(stream.getId(), data, false))
.thenAccept(s ->
{
// Disable checks for invalid headers.
((HTTP2Session)session).getGenerator().setValidateHpackEncoding(false);
// Invalid trailer: cannot contain pseudo headers.
HttpFields.Mutable trailerFields = HttpFields.build();
trailerFields.put(HttpHeader.C_METHOD, "GET");
MetaData trailer = new MetaData(HttpVersion.HTTP_2, trailerFields);
s.headers(new HeadersFrame(s.getId(), trailer, null, true));
});
assertTrue(serverLatch.await(5, TimeUnit.SECONDS));
assertTrue(clientLatch.await(5, TimeUnit.SECONDS));

View File

@ -26,7 +26,7 @@ import org.eclipse.jetty.quic.common.QuicStreamEndPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HTTP3StreamClient extends HTTP3Stream implements Stream.Client
public class HTTP3StreamClient extends HTTP3Stream implements Stream.Client
{
private static final Logger LOG = LoggerFactory.getLogger(HTTP3StreamClient.class);
@ -50,13 +50,14 @@ public class HTTP3StreamClient extends HTTP3Stream implements Stream.Client
public void onResponse(HeadersFrame frame)
{
MetaData.Response response = (MetaData.Response)frame.getMetaData();
boolean valid;
if (response.getStatus() == HttpStatus.CONTINUE_100)
valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL), FrameState.INFORMATIONAL);
else if (response.getStatus() == HttpStatus.EARLY_HINT_103)
valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.HEADER, FrameState.INFORMATIONAL), FrameState.INFORMATIONAL);
else
valid = validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.INFORMATIONAL), FrameState.HEADER);
int status = response.getStatus();
boolean valid = switch (status)
{
case HttpStatus.CONTINUE_100 -> validateAndUpdate(EnumSet.of(FrameState.INITIAL), FrameState.INFORMATIONAL);
case HttpStatus.PROCESSING_102,
HttpStatus.EARLY_HINT_103 -> validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.INFORMATIONAL), FrameState.INFORMATIONAL);
default -> validateAndUpdate(EnumSet.of(FrameState.INITIAL, FrameState.INFORMATIONAL), FrameState.HEADER);
};
if (valid)
{
onHeaders(frame);

View File

@ -394,9 +394,8 @@ public abstract class HTTP3Stream implements Stream, CyclicTimeouts.Expirable, A
public Promise.Completable<Stream> writeFrame(Frame frame)
{
notIdle();
Promise.Completable<Stream> completable = new Promise.Completable<>();
session.writeMessageFrame(endPoint.getStreamId(), frame, Callback.from(Invocable.InvocationType.NON_BLOCKING, () -> completable.succeeded(this), completable::failed));
return completable;
return Promise.Completable.with(p ->
session.writeMessageFrame(endPoint.getStreamId(), frame, Callback.from(Invocable.InvocationType.NON_BLOCKING, () -> p.succeeded(this), p::failed)));
}
public boolean isClosed()
@ -463,9 +462,42 @@ public abstract class HTTP3Stream implements Stream, CyclicTimeouts.Expirable, A
);
}
/**
* <p>Defines the state of the stream for received frames,</p>
* <p>allowing to verify that a frame sequence is valid for
* the HTTP protocol.</p>
* <p>For example, for a stream in the {@link #INITIAL} state,
* receiving a {@link DataFrame} would move the stream to the
* {@link #DATA} state which would be invalid, since for the
* HTTP protocol a {@link HeadersFrame} is expected before
* any {@link DataFrame}.</p>
*/
protected enum FrameState
{
INITIAL, INFORMATIONAL, HEADER, DATA, TRAILER, FAILED
/**
* The initial state of the stream, before it receives any frame.
*/
INITIAL,
/**
* The stream has received an HTTP informational response.
*/
INFORMATIONAL,
/**
* The stream has received an HTTP final response.
*/
HEADER,
/**
* The stream has received HTTP content.
*/
DATA,
/**
* The stream has received an HTTP trailer.
*/
TRAILER,
/**
* The stream has encountered a failure.
*/
FAILED
}
private enum CloseState

View File

@ -21,6 +21,9 @@ import java.util.function.Supplier;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersion;
@ -50,9 +53,11 @@ public class HttpStreamOverHTTP3 implements HttpStream
private final ServerHTTP3StreamConnection connection;
private final HttpChannel httpChannel;
private final HTTP3StreamServer stream;
private MetaData.Request requestMetaData;
private MetaData.Response responseMetaData;
private Content.Chunk chunk;
private MetaData.Response metaData;
private boolean committed;
private boolean expects100Continue;
public HttpStreamOverHTTP3(ServerHTTP3StreamConnection connection, HttpChannel httpChannel, HTTP3StreamServer stream)
{
@ -77,9 +82,9 @@ public class HttpStreamOverHTTP3 implements HttpStream
{
try
{
MetaData.Request request = (MetaData.Request)frame.getMetaData();
requestMetaData = (MetaData.Request)frame.getMetaData();
Runnable handler = httpChannel.onRequest(request);
Runnable handler = httpChannel.onRequest(requestMetaData);
if (frame.isLast())
{
@ -89,12 +94,11 @@ public class HttpStreamOverHTTP3 implements HttpStream
}
}
HttpFields fields = request.getFields();
HttpFields fields = requestMetaData.getFields();
// TODO: handle 100 continue.
// expect100Continue = fields.contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString());
expects100Continue = fields.contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString());
boolean connect = request instanceof MetaData.ConnectRequest;
boolean connect = requestMetaData instanceof MetaData.ConnectRequest;
if (!connect)
connection.setApplicationMode(true);
@ -103,7 +107,7 @@ public class HttpStreamOverHTTP3 implements HttpStream
{
LOG.debug("HTTP3 request #{}/{}, {} {} {}{}{}",
stream.getId(), Integer.toHexString(stream.getSession().hashCode()),
request.getMethod(), request.getURI(), request.getHttpVersion(),
requestMetaData.getMethod(), requestMetaData.getURI(), requestMetaData.getHttpVersion(),
System.lineSeparator(), fields);
}
@ -147,6 +151,12 @@ public class HttpStreamOverHTTP3 implements HttpStream
chunk = createChunk(data);
data.release();
// Some content is read, but the 100 Continue interim
// response has not been sent yet, then don't bother
// sending it later, as the client already sent the content.
if (expects100Continue && chunk.hasRemaining())
expects100Continue = false;
try (AutoLock ignored = lock.lock())
{
this.chunk = chunk;
@ -171,6 +181,11 @@ public class HttpStreamOverHTTP3 implements HttpStream
}
else
{
if (expects100Continue)
{
expects100Continue = false;
send(requestMetaData, HttpGenerator.CONTINUE_100_INFO, false, null, Callback.NOOP);
}
stream.demand();
}
}
@ -245,7 +260,7 @@ public class HttpStreamOverHTTP3 implements HttpStream
private void sendHeaders(MetaData.Request request, MetaData.Response response, ByteBuffer content, boolean lastContent, Callback callback)
{
metaData = response;
this.responseMetaData = response;
HeadersFrame headersFrame;
DataFrame dataFrame = null;
@ -256,12 +271,17 @@ public class HttpStreamOverHTTP3 implements HttpStream
if (HttpStatus.isInterim(response.getStatus()))
{
// Must not commit interim responses.
if (hasContent)
{
callback.failed(new IllegalStateException("Interim response cannot have content"));
return;
}
headersFrame = new HeadersFrame(metaData, false);
if (expects100Continue && response.getStatus() == HttpStatus.CONTINUE_100)
expects100Continue = false;
headersFrame = new HeadersFrame(response, false);
}
else
{
@ -272,7 +292,7 @@ public class HttpStreamOverHTTP3 implements HttpStream
long contentLength = response.getContentLength();
if (contentLength < 0)
{
metaData = new MetaData.Response(
this.responseMetaData = new MetaData.Response(
response.getHttpVersion(),
response.getStatus(),
response.getReason(),
@ -290,7 +310,7 @@ public class HttpStreamOverHTTP3 implements HttpStream
if (hasContent)
{
headersFrame = new HeadersFrame(metaData, false);
headersFrame = new HeadersFrame(response, false);
if (lastContent)
{
HttpFields trailers = retrieveTrailers();
@ -313,27 +333,27 @@ public class HttpStreamOverHTTP3 implements HttpStream
{
if (lastContent)
{
if (isTunnel(request, metaData))
if (isTunnel(request, response))
{
headersFrame = new HeadersFrame(metaData, false);
headersFrame = new HeadersFrame(response, false);
}
else
{
HttpFields trailers = retrieveTrailers();
if (trailers == null)
{
headersFrame = new HeadersFrame(metaData, true);
headersFrame = new HeadersFrame(response, true);
}
else
{
headersFrame = new HeadersFrame(metaData, false);
headersFrame = new HeadersFrame(response, false);
trailersFrame = new HeadersFrame(new MetaData(HttpVersion.HTTP_3, trailers), true);
}
}
}
else
{
headersFrame = new HeadersFrame(metaData, false);
headersFrame = new HeadersFrame(response, false);
}
}
}
@ -342,8 +362,8 @@ public class HttpStreamOverHTTP3 implements HttpStream
{
LOG.debug("HTTP3 Response #{}/{}:{}{} {}{}{}",
stream.getId(), Integer.toHexString(stream.getSession().hashCode()),
System.lineSeparator(), HttpVersion.HTTP_3, metaData.getStatus(),
System.lineSeparator(), metaData.getFields());
System.lineSeparator(), HttpVersion.HTTP_3, response.getStatus(),
System.lineSeparator(), response.getFields());
}
CompletableFuture<Stream> cf = stream.respond(headersFrame);
@ -363,7 +383,7 @@ public class HttpStreamOverHTTP3 implements HttpStream
{
boolean isHeadRequest = HttpMethod.HEAD.is(request.getMethod());
boolean hasContent = BufferUtil.hasContent(content) && !isHeadRequest;
if (hasContent || (lastContent && !isTunnel(request, metaData)))
if (hasContent || (lastContent && !isTunnel(request, responseMetaData)))
{
if (lastContent)
{
@ -398,7 +418,7 @@ public class HttpStreamOverHTTP3 implements HttpStream
private HttpFields retrieveTrailers()
{
Supplier<HttpFields> supplier = metaData.getTrailersSupplier();
Supplier<HttpFields> supplier = responseMetaData.getTrailersSupplier();
if (supplier == null)
return null;
HttpFields trailers = supplier.get();

View File

@ -26,9 +26,14 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jetty.client.ContinueProtocolHandler;
import org.eclipse.jetty.client.EarlyHintsProtocolHandler;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.ProcessingProtocolHandler;
import org.eclipse.jetty.client.ProtocolHandlers;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic;
import org.eclipse.jetty.client.util.AsyncRequestContent;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
@ -67,6 +72,8 @@ public abstract class ProxyHandler extends Handler.Processor
{
private static final Logger LOG = LoggerFactory.getLogger(ProxyHandler.class);
private static final String CLIENT_TO_PROXY_REQUEST_ATTRIBUTE = ProxyHandler.class.getName() + ".clientToProxyRequest";
private static final String PROXY_TO_CLIENT_RESPONSE_ATTRIBUTE = ProxyHandler.class.getName() + ".proxyToClientResponse";
private static final String PROXY_TO_SERVER_CONTINUE_ATTRIBUTE = ProxyHandler.class.getName() + ".proxyToServerContinue";
private static final EnumSet<HttpHeader> HOP_HEADERS = EnumSet.of(
HttpHeader.CONNECTION,
HttpHeader.KEEP_ALIVE,
@ -162,7 +169,11 @@ public abstract class ProxyHandler extends Handler.Processor
configureHttpClient(httpClient);
LifeCycle.start(httpClient);
httpClient.getContentDecoderFactories().clear();
httpClient.getProtocolHandlers().clear();
ProtocolHandlers protocolHandlers = httpClient.getProtocolHandlers();
protocolHandlers.clear();
protocolHandlers.put(new ProxyContinueProtocolHandler());
protocolHandlers.put(new ProxyProcessingProtocolHandler());
protocolHandlers.put(new ProxyEarlyHintsProtocolHandler());
return httpClient;
}
@ -204,7 +215,7 @@ public abstract class ProxyHandler extends Handler.Processor
{
if (LOG.isDebugEnabled())
LOG.debug("""
{} received C2P request
{} C2P received request
{}
{}""",
requestId(clientToProxyRequest),
@ -216,6 +227,8 @@ public abstract class ProxyHandler extends Handler.Processor
LOG.debug("{} URI rewrite {} => {}", requestId(clientToProxyRequest), clientToProxyRequest.getHttpURI(), rewritten);
var proxyToServerRequest = newProxyToServerRequest(clientToProxyRequest, rewritten);
proxyToServerRequest.attribute(CLIENT_TO_PROXY_REQUEST_ATTRIBUTE, clientToProxyRequest)
.attribute(PROXY_TO_CLIENT_RESPONSE_ATTRIBUTE, proxyToClientResponse);
copyRequestHeaders(clientToProxyRequest, proxyToServerRequest);
@ -225,7 +238,18 @@ public abstract class ProxyHandler extends Handler.Processor
{
if (expects100Continue(clientToProxyRequest))
{
// TODO
// Delay reading the content until the server sends 100 Continue.
AsyncRequestContent delayedProxyToServerRequestContent = new AsyncRequestContent();
proxyToServerRequest.body(delayedProxyToServerRequestContent);
Runnable action = () ->
{
if (LOG.isDebugEnabled())
LOG.debug("{} P2S continuing request", requestId(clientToProxyRequest));
var proxyToServerRequestContent = newProxyToServerRequestContent(clientToProxyRequest, proxyToClientResponse, proxyToServerRequest);
Content.copy(proxyToServerRequestContent, delayedProxyToServerRequestContent,
Callback.from(delayedProxyToServerRequestContent::close, x -> delayedProxyToServerRequestContent.write(Content.Chunk.from(x), Callback.NOOP)));
};
proxyToServerRequest.attribute(PROXY_TO_SERVER_CONTINUE_ATTRIBUTE, action);
}
else
{
@ -248,8 +272,7 @@ public abstract class ProxyHandler extends Handler.Processor
protected org.eclipse.jetty.client.api.Request newProxyToServerRequest(Request clientToProxyRequest, HttpURI newHttpURI)
{
return getHttpClient().newRequest(newHttpURI.toURI())
.method(clientToProxyRequest.getMethod())
.attribute(CLIENT_TO_PROXY_REQUEST_ATTRIBUTE, clientToProxyRequest);
.method(clientToProxyRequest.getMethod());
}
protected void copyRequestHeaders(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest)
@ -388,7 +411,7 @@ public abstract class ProxyHandler extends Handler.Processor
if (LOG.isDebugEnabled())
{
LOG.debug("""
{} sending P2S request
{} P2S sending request
{}
{}""",
requestId(clientToProxyRequest),
@ -417,6 +440,28 @@ public abstract class ProxyHandler extends Handler.Processor
Response.writeError(clientToProxyRequest, proxyToClientResponse, callback, status);
}
protected void onServerToProxyResponse100Continue(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest)
{
if (LOG.isDebugEnabled())
LOG.debug("{} P2C 100 continue response", requestId(clientToProxyRequest));
Runnable action = (Runnable)proxyToServerRequest.getAttributes().get(PROXY_TO_SERVER_CONTINUE_ATTRIBUTE);
action.run();
}
protected void onServerToProxyResponse102Processing(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest, HttpFields serverToProxyResponseHeaders, Response proxyToClientResponse)
{
if (LOG.isDebugEnabled())
LOG.debug("{} P2C 102 interim response {}", requestId(clientToProxyRequest), serverToProxyResponseHeaders);
proxyToClientResponse.writeInterim(HttpStatus.PROCESSING_102, serverToProxyResponseHeaders);
}
protected void onServerToProxyResponse103EarlyHints(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest, HttpFields serverToProxyResponseHeaders, Response proxyToClientResponse)
{
if (LOG.isDebugEnabled())
LOG.debug("{} P2C 103 interim response {}", requestId(clientToProxyRequest), serverToProxyResponseHeaders);
proxyToClientResponse.writeInterim(HttpStatus.EARLY_HINT_103, serverToProxyResponseHeaders);
}
protected void onProxyToClientResponseComplete(Request clientToProxyRequest, org.eclipse.jetty.client.api.Request proxyToServerRequest, org.eclipse.jetty.client.api.Response serverToProxyResponse, Response proxyToClientResponse, Callback proxyToClientCallback)
{
proxyToClientCallback.succeeded();
@ -519,7 +564,7 @@ public abstract class ProxyHandler extends Handler.Processor
{
Content.Chunk chunk = clientToProxyRequest.read();
if (LOG.isDebugEnabled())
LOG.debug("{} read C2P content {}", requestId(clientToProxyRequest), chunk);
LOG.debug("{} C2P read content {}", requestId(clientToProxyRequest), chunk);
return chunk;
}
@ -544,8 +589,7 @@ public abstract class ProxyHandler extends Handler.Processor
@Override
public boolean rewind()
{
// TODO: can this be delegated to the clientToProxyRequest?
return false;
return clientToProxyRequest.rewind();
}
}
@ -576,7 +620,7 @@ public abstract class ProxyHandler extends Handler.Processor
if (LOG.isDebugEnabled())
{
LOG.debug("""
{} received S2P response
{} S2P received response
{}
{}""",
requestId(clientToProxyRequest),
@ -595,7 +639,7 @@ public abstract class ProxyHandler extends Handler.Processor
if (LOG.isDebugEnabled())
{
LOG.debug("""
{} sending P2C response
{} P2C sending response
{}
{}""",
requestId(clientToProxyRequest),
@ -608,14 +652,14 @@ public abstract class ProxyHandler extends Handler.Processor
public void onContent(org.eclipse.jetty.client.api.Response serverToProxyResponse, ByteBuffer serverToProxyContent, Callback serverToProxyContentCallback)
{
if (LOG.isDebugEnabled())
LOG.debug("{} received S2P content {}", requestId(clientToProxyRequest), BufferUtil.toDetailString(serverToProxyContent));
LOG.debug("{} S2P received content {}", requestId(clientToProxyRequest), BufferUtil.toDetailString(serverToProxyContent));
Callback callback = new Callback()
{
@Override
public void succeeded()
{
if (LOG.isDebugEnabled())
LOG.debug("{} succeeded to write P2C content {}", requestId(clientToProxyRequest), BufferUtil.toDetailString(serverToProxyContent));
LOG.debug("{} P2C succeeded to write content {}", requestId(clientToProxyRequest), BufferUtil.toDetailString(serverToProxyContent));
serverToProxyContentCallback.succeeded();
}
@ -623,7 +667,7 @@ public abstract class ProxyHandler extends Handler.Processor
public void failed(Throwable failure)
{
if (LOG.isDebugEnabled())
LOG.debug("{} failed to write P2C content {}", requestId(clientToProxyRequest), BufferUtil.toDetailString(serverToProxyContent), failure);
LOG.debug("{} P2C failed to write content {}", requestId(clientToProxyRequest), BufferUtil.toDetailString(serverToProxyContent), failure);
serverToProxyContentCallback.failed(failure);
// Cannot write towards the client, abort towards the server.
serverToProxyResponse.abort(failure);
@ -714,4 +758,45 @@ public abstract class ProxyHandler extends Handler.Processor
return InvocationType.NON_BLOCKING;
}
}
private class ProxyContinueProtocolHandler extends ContinueProtocolHandler
{
@Override
protected void onContinue(org.eclipse.jetty.client.api.Request proxyToServerRequest)
{
super.onContinue(proxyToServerRequest);
var clientToProxyRequest = (Request)proxyToServerRequest.getAttributes().get(CLIENT_TO_PROXY_REQUEST_ATTRIBUTE);
if (LOG.isDebugEnabled())
LOG.debug("{} S2P received 100 Continue", requestId(clientToProxyRequest));
onServerToProxyResponse100Continue(clientToProxyRequest, proxyToServerRequest);
}
}
private class ProxyProcessingProtocolHandler extends ProcessingProtocolHandler
{
@Override
protected void onProcessing(org.eclipse.jetty.client.api.Request proxyToServerRequest, HttpFields serverToProxyResponseHeaders)
{
super.onProcessing(proxyToServerRequest, serverToProxyResponseHeaders);
var clientToProxyRequest = (Request)proxyToServerRequest.getAttributes().get(CLIENT_TO_PROXY_REQUEST_ATTRIBUTE);
if (LOG.isDebugEnabled())
LOG.debug("{} S2P received 102 Processing", requestId(clientToProxyRequest));
var proxyToClientResponse = (Response)proxyToServerRequest.getAttributes().get(PROXY_TO_CLIENT_RESPONSE_ATTRIBUTE);
onServerToProxyResponse102Processing(clientToProxyRequest, proxyToServerRequest, serverToProxyResponseHeaders, proxyToClientResponse);
}
}
private class ProxyEarlyHintsProtocolHandler extends EarlyHintsProtocolHandler
{
@Override
protected void onEarlyHints(org.eclipse.jetty.client.api.Request proxyToServerRequest, HttpFields serverToProxyResponseHeaders)
{
super.onEarlyHints(proxyToServerRequest, serverToProxyResponseHeaders);
var clientToProxyRequest = (Request)proxyToServerRequest.getAttributes().get(CLIENT_TO_PROXY_REQUEST_ATTRIBUTE);
if (LOG.isDebugEnabled())
LOG.debug("{} S2P received 103 Early Hints", requestId(clientToProxyRequest));
var proxyToClientResponse = (Response)proxyToServerRequest.getAttributes().get(PROXY_TO_CLIENT_RESPONSE_ATTRIBUTE);
onServerToProxyResponse103EarlyHints(clientToProxyRequest, proxyToServerRequest, serverToProxyResponseHeaders, proxyToClientResponse);
}
}
}

View File

@ -0,0 +1,92 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.proxy;
import java.util.List;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic;
import org.eclipse.jetty.client.http.HttpClientConnectionFactory;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http2.client.HTTP2Client;
import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.jupiter.api.AfterEach;
public class AbstractProxyTest
{
public static List<HttpVersion> httpVersions()
{
return List.of(HttpVersion.HTTP_1_1, HttpVersion.HTTP_2);
}
protected Server server;
protected ServerConnector serverConnector;
protected Server proxy;
protected ServerConnector proxyConnector;
protected HttpClient client;
protected void startClient() throws Exception
{
ClientConnector clientConnector = new ClientConnector();
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client");
clientConnector.setExecutor(clientThreads);
HTTP2Client http2Client = new HTTP2Client(clientConnector);
client = new HttpClient(new HttpClientTransportDynamic(clientConnector, HttpClientConnectionFactory.HTTP11, new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client)));
client.start();
}
protected void startProxy(ProxyHandler handler) throws Exception
{
QueuedThreadPool proxyPool = new QueuedThreadPool();
proxyPool.setName("proxy");
proxy = new Server(proxyPool);
HttpConfiguration configuration = new HttpConfiguration();
configuration.setSendDateHeader(false);
configuration.setSendServerVersion(false);
proxyConnector = new ServerConnector(proxy, 1, 1, new HttpConnectionFactory(configuration), new HTTP2CServerConnectionFactory(configuration));
proxy.addConnector(proxyConnector);
proxy.setHandler(handler);
proxy.start();
}
protected void startServer(Handler handler) throws Exception
{
QueuedThreadPool serverPool = new QueuedThreadPool();
serverPool.setName("server");
server = new Server(serverPool);
HttpConfiguration httpConfig = new HttpConfiguration();
serverConnector = new ServerConnector(server, 1, 1, new HttpConnectionFactory(httpConfig), new HTTP2CServerConnectionFactory(httpConfig));
server.addConnector(serverConnector);
server.setHandler(handler);
server.start();
}
@AfterEach
public void dispose()
{
LifeCycle.stop(client);
LifeCycle.stop(proxy);
LifeCycle.stop(server);
}
}

View File

@ -0,0 +1,67 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.proxy;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.util.StringRequestContent;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Promise;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class InterimResponseProxyTest extends AbstractProxyTest
{
@Test
public void testInterimResponses() throws Exception
{
startServer(new Handler.Processor()
{
@Override
public void process(Request request, Response response, Callback callback)
{
CompletableFuture<Void> completable = response.writeInterim(HttpStatus.CONTINUE_100, HttpFields.EMPTY)
.thenCompose(ignored -> Promise.Completable.<String>with(p -> Content.Source.asString(request, StandardCharsets.UTF_8, p)))
.thenCompose(content -> response.writeInterim(HttpStatus.PROCESSING_102, HttpFields.EMPTY).thenApply(ignored -> content))
.thenCompose(content -> response.writeInterim(HttpStatus.EARLY_HINT_103, HttpFields.EMPTY).thenApply(ignored -> content))
.thenCompose(content -> Callback.Completable.with(c -> Content.Sink.write(response, true, content, c)));
callback.completeWith(completable);
}
});
startProxy(new ProxyHandler.Reverse(clientToProxyRequest ->
HttpURI.build(clientToProxyRequest.getHttpURI()).port(serverConnector.getLocalPort())));
startClient();
String content = "hello world";
ContentResponse response = client.newRequest("localhost", proxyConnector.getLocalPort())
.headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE))
.body(new StringRequestContent(content))
.send();
assertEquals(content, response.getContentAsString());
}
}

View File

@ -13,8 +13,6 @@
package org.eclipse.jetty.proxy;
import java.util.List;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic;
@ -24,83 +22,20 @@ import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http2.client.HTTP2Client;
import org.eclipse.jetty.http2.client.transport.ClientConnectionFactoryOverHTTP2;
import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory;
import org.eclipse.jetty.io.ClientConnector;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnectionFactory;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ReverseProxyTest
public class ReverseProxyTest extends AbstractProxyTest
{
private Server server;
private ServerConnector serverConnector;
private Server proxy;
private ServerConnector proxyConnector;
private HttpClient client;
private void startServer(Handler handler) throws Exception
{
QueuedThreadPool serverPool = new QueuedThreadPool();
serverPool.setName("server");
server = new Server(serverPool);
HttpConfiguration httpConfig = new HttpConfiguration();
serverConnector = new ServerConnector(server, 1, 1, new HttpConnectionFactory(httpConfig), new HTTP2CServerConnectionFactory(httpConfig));
server.addConnector(serverConnector);
server.setHandler(handler);
server.start();
}
private void startProxy(Handler handler) throws Exception
{
QueuedThreadPool proxyPool = new QueuedThreadPool();
proxyPool.setName("proxy");
proxy = new Server(proxyPool);
HttpConfiguration configuration = new HttpConfiguration();
configuration.setSendDateHeader(false);
configuration.setSendServerVersion(false);
proxyConnector = new ServerConnector(proxy, 1, 1, new HttpConnectionFactory(configuration), new HTTP2CServerConnectionFactory(configuration));
proxy.addConnector(proxyConnector);
proxy.setHandler(handler);
proxy.start();
}
private void startClient() throws Exception
{
ClientConnector clientConnector = new ClientConnector();
QueuedThreadPool clientThreads = new QueuedThreadPool();
clientThreads.setName("client");
clientConnector.setExecutor(clientThreads);
HTTP2Client http2Client = new HTTP2Client(clientConnector);
client = new HttpClient(new HttpClientTransportDynamic(clientConnector, HttpClientConnectionFactory.HTTP11, new ClientConnectionFactoryOverHTTP2.HTTP2(http2Client)));
client.start();
}
@AfterEach
public void dispose()
{
LifeCycle.stop(client);
LifeCycle.stop(proxy);
LifeCycle.stop(server);
}
private static List<HttpVersion> httpVersions()
{
return List.of(HttpVersion.HTTP_1_1, HttpVersion.HTTP_2);
}
@ParameterizedTest
@MethodSource("httpVersions")
public void testSimple(HttpVersion httpVersion) throws Exception

View File

@ -20,7 +20,7 @@
<profile>
<id>jdk17</id>
<activation>
<jdk>[17,)</jdk>
<jdk>17</jdk>
</activation>
<modules>
<module>jetty-quic-quiche-foreign-incubator</module>

View File

@ -56,6 +56,11 @@
<artifactId>jetty-test-helper</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.toolchain</groupId>
<artifactId>jetty-xhtml-schemas</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-http-tools</artifactId>

View File

@ -534,9 +534,7 @@ public interface Handler extends LifeCycle, Destroyable, Invocable
public List<Handler> getHandlers()
{
Handler next = getHandler();
if (next == null)
return Collections.emptyList();
return List.of(next);
return (next == null) ? Collections.emptyList() : Collections.singletonList(next);
}
@Override

View File

@ -112,9 +112,8 @@ public class OptionalSslConnectionFactory extends DetectorConnectionFactory
"Connection: close\r\n" +
"\r\n" +
body;
Callback.Completable completable = new Callback.Completable();
endPoint.write(completable, ByteBuffer.wrap(response.getBytes(StandardCharsets.US_ASCII)));
completable.whenComplete((r, x) -> endPoint.close());
Callback.Completable.with(c -> endPoint.write(c, ByteBuffer.wrap(response.getBytes(StandardCharsets.US_ASCII))))
.whenComplete((r, x) -> endPoint.close());
}
else
{

View File

@ -44,7 +44,7 @@ public class ResourceListing
public static final Logger LOG = LoggerFactory.getLogger(ResourceListing.class);
/**
* Convert the Resource directory into an HTML directory listing.
* Convert the Resource directory into an XHTML directory listing.
*
* @param resource the resource to build the listing from
* @param base The base URL
@ -52,7 +52,7 @@ public class ResourceListing
* @param query query params
* @return the HTML as String
*/
public static String getAsHTML(Resource resource, String base, boolean parent, String query)
public static String getAsXHTML(Resource resource, String base, boolean parent, String query)
{
// This method doesn't check aliases, so it is OK to canonicalize here.
base = URIUtil.normalizePath(base);
@ -105,13 +105,15 @@ public class ResourceListing
StringBuilder buf = new StringBuilder(4096);
// Doctype Declaration (HTML5)
buf.append("<!DOCTYPE html>\n");
buf.append("<html lang=\"en\">\n");
// Doctype Declaration + XHTML
buf.append("""
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
""");
// HTML Header
buf.append("<head>\n");
buf.append("<meta charset=\"utf-8\">\n");
buf.append("<link href=\"jetty-dir.css\" rel=\"stylesheet\" />\n");
buf.append("<title>");
buf.append(title);
@ -145,7 +147,7 @@ public class ResourceListing
}
}
buf.append("<tr><th class=\"name\"><a href=\"?C=N&O=").append(order).append("\">");
buf.append("<tr><th class=\"name\"><a href=\"?C=N&amp;O=").append(order).append("\">");
buf.append("Name").append(arrow);
buf.append("</a></th>");
@ -165,7 +167,7 @@ public class ResourceListing
}
}
buf.append("<th class=\"lastmodified\"><a href=\"?C=M&O=").append(order).append("\">");
buf.append("<th class=\"lastmodified\"><a href=\"?C=M&amp;O=").append(order).append("\">");
buf.append("Last Modified").append(arrow);
buf.append("</a></th>");
@ -184,7 +186,7 @@ public class ResourceListing
arrow = ARROW_DOWN;
}
}
buf.append("<th class=\"size\"><a href=\"?C=S&O=").append(order).append("\">");
buf.append("<th class=\"size\"><a href=\"?C=S&amp;O=").append(order).append("\">");
buf.append("Size").append(arrow);
buf.append("</a></th></tr>\n");
buf.append("</thead>\n");
@ -197,6 +199,7 @@ public class ResourceListing
{
// Name
buf.append("<tr><td class=\"name\"><a href=\"");
// TODO This produces an absolute link from the /context/<listing-dir> path, investigate if we can use relative links reliably now
buf.append(URIUtil.addPaths(encodedBase, "../"));
buf.append("\">Parent Directory</a></td>");
// Last Modified
@ -228,8 +231,7 @@ public class ResourceListing
buf.append(path);
buf.append("\">");
buf.append(deTag(name));
buf.append("&nbsp;");
buf.append("</a></td>");
buf.append("&nbsp;</a></td>");
// Last Modified
buf.append("<td class=\"lastmodified\">");
@ -262,11 +264,18 @@ public class ResourceListing
}
/**
* <p>
* Encode any characters that could break the URI string in an HREF.
* Such as <a href="/path/to;<script>Window.alert("XSS"+'%20'+"here");</script>">Link</a>
* </p>
*
* <p>
* Such as:
* {@code <a href="/path/to;<script>Window.alert('XSS'+'%20'+'here');</script>">Link</a>}
* </p>
* <p>
* The above example would parse incorrectly on various browsers as the "<" or '"' characters
* would end the href attribute value string prematurely.
* </p>
*
* @param raw the raw text to encode.
* @return the defanged text.

View File

@ -559,7 +559,7 @@ public class ResourceService
}
String base = URIUtil.addEncodedPaths(request.getHttpURI().getPath(), URIUtil.SLASH);
String listing = ResourceListing.getAsHTML(httpContent.getResource(), base, pathInContext.length() > 1, request.getHttpURI().getQuery());
String listing = ResourceListing.getAsXHTML(httpContent.getResource(), base, pathInContext.length() > 1, request.getHttpURI().getQuery());
if (listing == null)
{
writeHttpError(request, response, callback, HttpStatus.FORBIDDEN_403);

View File

@ -15,6 +15,7 @@ package org.eclipse.jetty.server;
import java.nio.ByteBuffer;
import java.util.ListIterator;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -62,6 +63,9 @@ public interface Response extends Content.Sink
void reset();
CompletableFuture<Void> writeInterim(int status, HttpFields headers);
// TODO: make it static, otherwise we must override it in Wrapper.
default boolean writeTrailers(Content.Chunk chunk, Callback ignored)
{
if (chunk instanceof Trailers trailers)
@ -156,7 +160,6 @@ public interface Response extends Content.Sink
if (field.getHeader() == HttpHeader.SET_COOKIE)
{
CookieCompliance compliance = httpConfiguration.getResponseCookieCompliance();
HttpCookie oldCookie;
if (field instanceof HttpCookie.SetCookieHttpField)
{
if (!HttpCookie.match(((HttpCookie.SetCookieHttpField)field).getHttpCookie(), cookie.getName(), cookie.getDomain(), cookie.getPath()))
@ -203,8 +206,6 @@ public interface Response extends Content.Sink
static void writeError(Request request, Response response, Callback callback, int status, String message, Throwable cause)
{
// TODO what about 102 Processing?
// Retrieve the Logger instance here, rather than having a
// public field that will force a transitive dependency on SLF4J.
Logger logger = LoggerFactory.getLogger(Response.class);
@ -420,5 +421,11 @@ public interface Response extends Content.Sink
{
getWrapped().reset();
}
@Override
public CompletableFuture<Void> writeInterim(int status, HttpFields headers)
{
return getWrapped().writeInterim(status, headers);
}
}
}

View File

@ -855,7 +855,7 @@ public class ContextHandler extends Handler.Wrapper implements Attributes, Grace
if (resource.isAlias())
{
if (LOG.isDebugEnabled())
LOG.debug("Aliased resource: {}~={}", resource, resource.getAlias());
LOG.debug("Aliased resource: {} -> {}", resource, resource.getTargetURI());
// alias checks
for (AliasCheck check : _aliasChecks)

View File

@ -13,13 +13,113 @@
package org.eclipse.jetty.server.handler;
import java.util.Collections;
import java.util.List;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.thread.Invocable;
/**
* A <code>HandlerContainer</code> that allows a hot swap of a wrapped handler.
* @deprecated
*/
@Deprecated
public class HotSwapHandler extends Handler.Wrapper
public class HotSwapHandler extends Handler.AbstractContainer implements Handler.Nested
{
// TODO unit tests
private volatile Handler _handler;
/**
*
*/
public HotSwapHandler()
{
}
/**
* @return Returns the handlers.
*/
public Handler getHandler()
{
return _handler;
}
/**
* @return Returns the handlers.
*/
@Override
public List<Handler> getHandlers()
{
Handler next = _handler;
return (next == null) ? Collections.emptyList() : Collections.singletonList(next);
}
/**
* @param handler Set the {@link Handler} which should be wrapped.
*/
public void setHandler(Handler handler)
{
// check state
Server server1 = ((Nested)this).getServer();
if (server1 != null && server1.isStarted() && handler != null &&
server1.getInvocationType() != Invocable.combine(server1.getInvocationType(), handler.getInvocationType()))
throw new IllegalArgumentException("Cannot change invocation type of started server");
// Check for loops.
if (handler == this || (handler instanceof Container container &&
container.getDescendants().contains(this)))
throw new IllegalStateException("setHandler loop");
try
{
Server server = getServer();
if (handler == _handler)
return;
Handler oldHandler = _handler;
if (handler != null)
{
handler.setServer(server);
addBean(handler, true);
if (oldHandler != null && oldHandler.isStarted())
handler.start();
}
_handler = handler;
if (oldHandler != null)
removeBean(oldHandler);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
@Override
public Request.Processor handle(Request request) throws Exception
{
Handler next = _handler;
return next == null ? null : next.handle(request);
}
@Override
public InvocationType getInvocationType()
{
Handler next = getHandler();
return next == null ? InvocationType.NON_BLOCKING : next.getInvocationType();
}
@Override
public void destroy()
{
if (!isStopped())
throw new IllegalStateException("!STOPPED");
Handler child = getHandler();
if (child != null)
{
setHandler((Handler)null);
child.destroy();
}
super.destroy();
}
}

View File

@ -21,6 +21,7 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Function;
import java.util.function.Predicate;
@ -1263,6 +1264,24 @@ public class HttpChannelState implements HttpChannel, Components
_request.getHttpChannel().resetResponse();
}
@Override
public CompletableFuture<Void> writeInterim(int status, HttpFields headers)
{
Completable completable = new Completable();
if (HttpStatus.isInterim(status))
{
HttpChannelState channel = _request.getHttpChannel();
HttpVersion version = channel.getConnectionMetaData().getHttpVersion();
MetaData.Response response = new MetaData.Response(version, status, headers);
channel._stream.send(_request._metaData, response, false, null, completable);
}
else
{
completable.failed(new IllegalArgumentException("Invalid interim status code: " + status));
}
return completable;
}
private MetaData.Response lockedPrepareResponse(HttpChannelState httpChannel, boolean last)
{
// Assume 200 unless told otherwise.

View File

@ -1157,14 +1157,12 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
private MetaData.Request _request;
private HttpField _upgrade = null;
private Connection _upgradeConnection;
Content.Chunk _chunk;
private Content.Chunk _chunk;
private boolean _connectionClose = false;
private boolean _connectionKeepAlive = false;
private boolean _connectionUpgrade = false;
private boolean _unknownExpectation = false;
private boolean _expect100Continue = false;
private boolean _expect102Processing = false;
private boolean _expects100Continue = false;
private List<String> _complianceViolations;
protected HttpStreamOverHTTP1(String method, String uri, HttpVersion version)
@ -1206,22 +1204,16 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
{
if (!HttpHeaderValue.parseCsvIndex(value, t ->
{
switch (t)
if (t == HttpHeaderValue.CONTINUE)
{
case CONTINUE:
_expect100Continue = true;
return true;
case PROCESSING:
_expect102Processing = true;
return true;
default:
return false;
_expects100Continue = true;
return true;
}
return false;
}, s -> false))
{
_unknownExpectation = true;
_expect100Continue = false;
_expect102Processing = false;
_expects100Continue = false;
}
break;
}
@ -1398,8 +1390,12 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
Content.Chunk content = _chunk;
_chunk = Content.Chunk.next(content);
if (content != null && _expect100Continue && content.hasRemaining())
_expect100Continue = false;
// Some content is read, but the 100 Continue interim
// response has not been sent yet, then don't bother
// sending it later, as the client already sent the content.
if (content != null && _expects100Continue && content.hasRemaining())
_expects100Continue = false;
return content;
}
@ -1423,9 +1419,9 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
return;
}
if (_expect100Continue)
if (_expects100Continue)
{
_expect100Continue = false;
_expects100Continue = false;
send(_request, HttpGenerator.CONTINUE_100_INFO, false, null, Callback.NOOP);
}
@ -1454,15 +1450,18 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
{
callback.failed(new IllegalStateException("Committed"));
}
else if (response.getStatus() == 102 && !_expect102Processing)
else if (_expects100Continue)
{
// silently discard
callback.succeeded();
}
else if (response.getStatus() != 100 && _expect100Continue)
{
// If we are still expecting a 100 continues when we commit then we can't be persistent
_generator.setPersistent(false);
if (response.getStatus() == HttpStatus.CONTINUE_100)
{
_expects100Continue = false;
}
else
{
// Expecting to send a 100 Continue response, but it's a different response,
// then cannot be persistent because likely the client did not send the content.
_generator.setPersistent(false);
}
}
if (_sendCallback.reset(_request, response, content, last, callback))
@ -1586,11 +1585,11 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
if (HttpConnection.this.upgrade(stream))
return;
// Finish consuming the request
// If we are still expecting
if (_expect100Continue)
if (_expects100Continue)
{
// close to seek EOF
// No content was read, and no 100 Continue response was sent.
// Close the parser so that below it seeks EOF, not the next request.
_expects100Continue = false;
_parser.close();
}
@ -1613,8 +1612,6 @@ public class HttpConnection extends AbstractConnection implements Runnable, Writ
if (LOG.isDebugEnabled())
LOG.debug("non-current completion {}", this);
// TODO what about upgrade????
// If we are looking for the next request
if (_parser.isStart())
{

View File

@ -369,9 +369,8 @@ public class DetectorConnectionTest
// omitting this will leak the buffer
connector.getByteBufferPool().release(buffer);
Callback.Completable completable = new Callback.Completable();
endPoint.write(completable, ByteBuffer.wrap("No upgrade for you".getBytes(StandardCharsets.US_ASCII)));
completable.whenComplete((r, x) -> endPoint.close());
Callback.Completable.with(c -> endPoint.write(c, ByteBuffer.wrap("No upgrade for you".getBytes(StandardCharsets.US_ASCII))))
.whenComplete((r, x) -> endPoint.close());
}
};
HttpConnectionFactory http = new HttpConnectionFactory();

View File

@ -279,7 +279,7 @@ public class LocalConnectorTest
}
@Test
public void testExpectContinuesAvailable() throws Exception
public void testExpect100ContinueContentAvailable() throws Exception
{
LocalConnector.LocalEndPoint endp = _connector.connect();
endp.addInput(
@ -299,7 +299,7 @@ public class LocalConnectorTest
}
@Test
public void testExpectContinues() throws Exception
public void testExpect100Continue() throws Exception
{
LocalConnector.LocalEndPoint endp = _connector.executeRequest(
"""

View File

@ -0,0 +1,497 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.server;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import javax.xml.catalog.Catalog;
import javax.xml.catalog.CatalogManager;
import javax.xml.catalog.CatalogResolver;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.toolchain.xhtml.CatalogXHTML;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.junit.jupiter.api.Test;
import org.w3c.dom.Document;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
public class ResourceListingTest
{
@Test
public void testBasicResourceXHtmlListingRoot(WorkDir workDir) throws IOException
{
Path root = workDir.getEmptyPathDir();
FS.touch(root.resolve("entry1.txt"));
FS.touch(root.resolve("entry2.dat"));
Files.createDirectory(root.resolve("dirFoo"));
Files.createDirectory(root.resolve("dirBar"));
Files.createDirectory(root.resolve("dirZed"));
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource resource = resourceFactory.newResource(root);
String content = ResourceListing.getAsXHTML(resource, "/", false, null);
assertTrue(isValidXHtml(content));
assertThat(content, containsString("entry1.txt"));
assertThat(content, containsString("<a href=\"/entry1.txt\">"));
assertThat(content, containsString("entry2.dat"));
assertThat(content, containsString("<a href=\"/entry2.dat\">"));
assertThat(content, containsString("dirFoo/"));
assertThat(content, containsString("<a href=\"/dirFoo/\">"));
assertThat(content, containsString("dirBar/"));
assertThat(content, containsString("<a href=\"/dirBar/\">"));
assertThat(content, containsString("dirZed/"));
assertThat(content, containsString("<a href=\"/dirZed/\">"));
}
}
@Test
public void testBasicResourceXHtmlListingDeep(WorkDir workDir) throws IOException
{
Path root = workDir.getEmptyPathDir();
FS.touch(root.resolve("entry1.txt"));
FS.touch(root.resolve("entry2.dat"));
Files.createDirectory(root.resolve("dirFoo"));
Files.createDirectory(root.resolve("dirBar"));
Files.createDirectory(root.resolve("dirZed"));
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource resource = resourceFactory.newResource(root);
String content = ResourceListing.getAsXHTML(resource, "/deep/", false, null);
assertTrue(isValidXHtml(content));
assertThat(content, containsString("entry1.txt"));
assertThat(content, containsString("<a href=\"/deep/entry1.txt\">"));
assertThat(content, containsString("entry2.dat"));
assertThat(content, containsString("<a href=\"/deep/entry2.dat\">"));
assertThat(content, containsString("dirFoo/"));
assertThat(content, containsString("<a href=\"/deep/dirFoo/\">"));
assertThat(content, containsString("dirBar/"));
assertThat(content, containsString("<a href=\"/deep/dirBar/\">"));
assertThat(content, containsString("dirZed/"));
assertThat(content, containsString("<a href=\"/deep/dirZed/\">"));
}
}
@Test
public void testResourceCollectionXHtmlListingContext(WorkDir workDir) throws IOException
{
Path root = workDir.getEmptyPathDir();
Path docrootA = root.resolve("docrootA");
Files.createDirectory(docrootA);
FS.touch(docrootA.resolve("entry1.txt"));
FS.touch(docrootA.resolve("entry2.dat"));
FS.touch(docrootA.resolve("similar.txt"));
Files.createDirectory(docrootA.resolve("dirSame"));
Files.createDirectory(docrootA.resolve("dirFoo"));
Files.createDirectory(docrootA.resolve("dirBar"));
Path docrootB = root.resolve("docrootB");
Files.createDirectory(docrootB);
FS.touch(docrootB.resolve("entry3.png"));
FS.touch(docrootB.resolve("entry4.tar.gz"));
FS.touch(docrootB.resolve("similar.txt")); // same filename as in docrootA
Files.createDirectory(docrootB.resolve("dirSame")); // same directory name as in docrootA
Files.createDirectory(docrootB.resolve("dirCid"));
Files.createDirectory(docrootB.resolve("dirZed"));
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
List<URI> uriRootList = List.of(docrootA.toUri(), docrootB.toUri());
Resource resource = resourceFactory.newResource(uriRootList);
String content = ResourceListing.getAsXHTML(resource, "/context/", false, null);
assertTrue(isValidXHtml(content));
assertThat(content, containsString("entry1.txt"));
assertThat(content, containsString("<a href=\"/context/entry1.txt\">"));
assertThat(content, containsString("entry2.dat"));
assertThat(content, containsString("<a href=\"/context/entry2.dat\">"));
assertThat(content, containsString("entry3.png"));
assertThat(content, containsString("<a href=\"/context/entry3.png\">"));
assertThat(content, containsString("entry4.tar.gz"));
assertThat(content, containsString("<a href=\"/context/entry4.tar.gz\">"));
assertThat(content, containsString("dirFoo/"));
assertThat(content, containsString("<a href=\"/context/dirFoo/\">"));
assertThat(content, containsString("dirBar/"));
assertThat(content, containsString("<a href=\"/context/dirBar/\">"));
assertThat(content, containsString("dirCid/"));
assertThat(content, containsString("<a href=\"/context/dirCid/\">"));
assertThat(content, containsString("dirZed/"));
assertThat(content, containsString("<a href=\"/context/dirZed/\">"));
int count;
// how many dirSame links do we have?
count = content.split(Pattern.quote("<a href=\"/context/dirSame/\">"), -1).length - 1;
assertThat(count, is(1));
// how many similar.txt do we have?
count = content.split(Pattern.quote("<a href=\"/context/similar.txt\">"), -1).length - 1;
assertThat(count, is(1));
}
}
@Test
public void testResourceCollectionMixedTypesXHtmlListingContext(WorkDir workDir) throws IOException
{
Path root = workDir.getEmptyPathDir();
Path docrootA = root.resolve("docrootA");
Files.createDirectory(docrootA);
FS.touch(docrootA.resolve("entry1.txt"));
FS.touch(docrootA.resolve("entry2.dat"));
Files.createDirectory(docrootA.resolve("dirFoo"));
Files.createDirectory(docrootA.resolve("dirBar"));
Path docrootB = root.resolve("docrootB");
Files.createDirectory(docrootB);
FS.touch(docrootB.resolve("entry3.png"));
FS.touch(docrootB.resolve("entry4.tar.gz"));
Files.createDirectory(docrootB.resolve("dirCid"));
Files.createDirectory(docrootB.resolve("dirZed"));
// Introduce a non-directory entry
Path docNonRootC = root.resolve("non-root.dat");
FS.touch(docNonRootC);
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
// Collection consisting of file, dir, dir
List<URI> uriRootList = List.of(docNonRootC.toUri(), docrootA.toUri(), docrootB.toUri());
Resource resource = resourceFactory.newResource(uriRootList);
String content = ResourceListing.getAsXHTML(resource, "/context/", false, null);
assertTrue(isValidXHtml(content));
assertThat(content, containsString("entry1.txt"));
assertThat(content, containsString("<a href=\"/context/entry1.txt\">"));
assertThat(content, containsString("entry2.dat"));
assertThat(content, containsString("<a href=\"/context/entry2.dat\">"));
assertThat(content, containsString("entry3.png"));
assertThat(content, containsString("<a href=\"/context/entry3.png\">"));
assertThat(content, containsString("entry4.tar.gz"));
assertThat(content, containsString("<a href=\"/context/entry4.tar.gz\">"));
assertThat(content, containsString("dirFoo/"));
assertThat(content, containsString("<a href=\"/context/dirFoo/\">"));
assertThat(content, containsString("dirBar/"));
assertThat(content, containsString("<a href=\"/context/dirBar/\">"));
assertThat(content, containsString("dirCid/"));
assertThat(content, containsString("<a href=\"/context/dirCid/\">"));
assertThat(content, containsString("dirZed/"));
assertThat(content, containsString("<a href=\"/context/dirZed/\">"));
assertThat(content, containsString("<a href=\"/context/non-root.dat\">"));
}
}
/**
* Test a ResourceCollection that is constructed by nested ResourceCollection.
*/
@Test
public void testResourceCollectionNestedXHtmlListingContext(WorkDir workDir) throws IOException
{
Path root = workDir.getEmptyPathDir();
Path docrootA = root.resolve("docrootA");
Files.createDirectory(docrootA);
FS.touch(docrootA.resolve("entry1.txt"));
FS.touch(docrootA.resolve("entry2.dat"));
Files.createDirectory(docrootA.resolve("dirFoo"));
Files.createDirectory(docrootA.resolve("dirBar"));
Path docrootB = root.resolve("docrootB");
Files.createDirectory(docrootB);
FS.touch(docrootB.resolve("entry3.png"));
FS.touch(docrootB.resolve("entry4.tar.gz"));
Files.createDirectory(docrootB.resolve("dirCid"));
Files.createDirectory(docrootB.resolve("dirZed"));
// Introduce a non-directory entry
Path docNonRootC = root.resolve("non-root.dat");
FS.touch(docNonRootC);
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
// Collection consisting of inputs [file, dir]
Resource resourceCollectionA = resourceFactory.newResource(List.of(docNonRootC.toUri(), docrootB.toUri()));
// Basic resource, just a dir
Resource basicResource = resourceFactory.newResource(docrootA);
// New Collection consisting of inputs [collection, dir] - resulting in [file, dir, dir]
Resource resourceCollectionB = Resource.combine(resourceCollectionA, basicResource);
// Use collection in generating the output
String content = ResourceListing.getAsXHTML(resourceCollectionB, "/context/", false, null);
assertTrue(isValidXHtml(content));
assertThat(content, containsString("entry1.txt"));
assertThat(content, containsString("<a href=\"/context/entry1.txt\">"));
assertThat(content, containsString("entry2.dat"));
assertThat(content, containsString("<a href=\"/context/entry2.dat\">"));
assertThat(content, containsString("entry3.png"));
assertThat(content, containsString("<a href=\"/context/entry3.png\">"));
assertThat(content, containsString("entry4.tar.gz"));
assertThat(content, containsString("<a href=\"/context/entry4.tar.gz\">"));
assertThat(content, containsString("dirFoo/"));
assertThat(content, containsString("<a href=\"/context/dirFoo/\">"));
assertThat(content, containsString("dirBar/"));
assertThat(content, containsString("<a href=\"/context/dirBar/\">"));
assertThat(content, containsString("dirCid/"));
assertThat(content, containsString("<a href=\"/context/dirCid/\">"));
assertThat(content, containsString("dirZed/"));
assertThat(content, containsString("<a href=\"/context/dirZed/\">"));
assertThat(content, containsString("<a href=\"/context/non-root.dat\">"));
}
}
/**
* A regression on windows allowed the directory listing show
* the fully qualified paths within the directory listing.
* This test ensures that this behavior will not arise again.
*/
@Test
public void testListingFilenamesOnly(WorkDir workDir) throws Exception
{
Path docRoot = workDir.getEmptyPathDir();
/* create some content in the docroot */
FS.ensureDirExists(docRoot);
Path one = docRoot.resolve("one");
FS.ensureDirExists(one);
Path deep = one.resolve("deep");
FS.ensureDirExists(deep);
FS.touch(deep.resolve("foo"));
FS.ensureDirExists(docRoot.resolve("two"));
FS.ensureDirExists(docRoot.resolve("three"));
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource resourceBase = resourceFactory.newResource(docRoot);
Resource resource = resourceBase.resolve("one/deep/");
String content = ResourceListing.getAsXHTML(resource, "/context/", false, null);
assertTrue(isValidXHtml(content));
assertThat(content, containsString("/foo"));
String resBasePath = docRoot.toAbsolutePath().toString();
assertThat(content, not(containsString(resBasePath)));
}
}
@Test
public void testListingProperUrlEncoding(WorkDir workDir) throws Exception
{
Path docRoot = workDir.getEmptyPathDir();
/* create some content in the docroot */
Path wackyDir = docRoot.resolve("dir;"); // this should not be double-encoded.
FS.ensureDirExists(wackyDir);
FS.ensureDirExists(wackyDir.resolve("four"));
FS.ensureDirExists(wackyDir.resolve("five"));
FS.ensureDirExists(wackyDir.resolve("six"));
/* At this point we have the following
* testListingProperUrlEncoding/
* `-- docroot
* `-- dir;
* |-- five
* |-- four
* `-- six
*/
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource resourceBase = resourceFactory.newResource(docRoot);
// Resolve directory
Resource resource = resourceBase.resolve("dir%3B");
// Context
String content = ResourceListing.getAsXHTML(resource, "/context/dir%3B/", false, null);
assertTrue(isValidXHtml(content));
// Should not see double-encoded ";"
// First encoding: ";" -> "%3B"
// Second encoding: "%3B" -> "%253B" (BAD!)
assertThat(content, not(containsString("%253B")));
assertThat(content, containsString("/dir%3B/"));
assertThat(content, containsString("/dir%3B/four/"));
assertThat(content, containsString("/dir%3B/five/"));
assertThat(content, containsString("/dir%3B/six/"));
}
}
@Test
public void testListingWithQuestionMarks(WorkDir workDir) throws Exception
{
Path docRoot = workDir.getEmptyPathDir();
/* create some content in the docroot */
FS.ensureDirExists(docRoot.resolve("one"));
FS.ensureDirExists(docRoot.resolve("two"));
FS.ensureDirExists(docRoot.resolve("three"));
// Creating dir 'f??r' (Might not work in Windows)
assumeMkDirSupported(docRoot, "f??r");
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource resource = resourceFactory.newResource(docRoot);
String content = ResourceListing.getAsXHTML(resource, "/context/", false, null);
assertTrue(isValidXHtml(content));
assertThat(content, containsString("f??r"));
}
}
@Test
public void testListingEncoding(WorkDir workDir) throws Exception
{
Path docRoot = workDir.getEmptyPathDir();
/* create some content in the docroot */
Path one = docRoot.resolve("one");
FS.ensureDirExists(one);
// example of content on disk that could cause problems when taken to the HTML space.
Path alert = one.resolve("onmouseclick='alert(oops)'");
FS.touch(alert);
try (ResourceFactory.Closeable resourceFactory = ResourceFactory.closeable())
{
Resource resourceBase = resourceFactory.newResource(docRoot);
Resource resource = resourceBase.resolve("one");
String content = ResourceListing.getAsXHTML(resource, "/context/one", false, null);
assertTrue(isValidXHtml(content));
// Entry should be properly encoded
assertThat(content, containsString("<a href=\"/context/one/onmouseclick=%27alert(oops)%27\">"));
}
}
private static boolean isValidXHtml(String content)
{
// we expect that our generated output conforms to text/xhtml is well-formed
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)))
{
Catalog catalog = CatalogXHTML.getCatalog();
CatalogResolver resolver = CatalogManager.catalogResolver(catalog);
DocumentBuilderFactory xmlDocumentBuilderFactory = DocumentBuilderFactory.newInstance();
xmlDocumentBuilderFactory.setValidating(true);
DocumentBuilder db = xmlDocumentBuilderFactory.newDocumentBuilder();
db.setEntityResolver(resolver);
List<SAXParseException> errors = new ArrayList<>();
db.setErrorHandler(new ErrorHandler()
{
@Override
public void warning(SAXParseException exception)
{
exception.printStackTrace();
}
@Override
public void error(SAXParseException exception)
{
errors.add(exception);
}
@Override
public void fatalError(SAXParseException exception)
{
errors.add(exception);
}
});
// We consider this content to be XML well-formed if these 2 lines do not throw an Exception
Document doc = db.parse(inputStream);
doc.getDocumentElement().normalize();
if (errors.size() > 0)
{
IOException ioException = new IOException("Failed to validate XHTML");
for (SAXException saxException : errors)
{
ioException.addSuppressed(saxException);
}
fail(ioException);
}
return true; // it's well-formed
}
catch (IOException | ParserConfigurationException | SAXException e)
{
e.printStackTrace(System.err);
return false; // XHTML has got issues
}
}
/**
* Attempt to create the directory, skip testcase if not supported on OS.
*/
private static Path assumeMkDirSupported(Path path, String subpath)
{
Path ret = null;
try
{
ret = path.resolve(subpath);
if (Files.exists(ret))
return ret;
Files.createDirectories(ret);
}
catch (InvalidPathException | IOException ignore)
{
// ignore
}
assumeTrue(ret != null, "Directory creation not supported on OS: " + path + File.separator + subpath);
assumeTrue(Files.exists(ret), "Directory creation not supported on OS: " + ret);
return ret;
}
}

View File

@ -344,7 +344,7 @@ public class ContextHandlerGetResourceTest
Resource resource = context.getResource(path);
assertNotNull(resource);
assertEquals(context.getResource("/subdir/TextFile.Long.txt").getURI(), resource.getAlias());
assertEquals(context.getResource("/subdir/TextFile.Long.txt").getURI(), resource.getTargetURI());
URL url = context.getServletContext().getResource(path);
assertNotNull(url);

View File

@ -42,6 +42,7 @@ import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.http.UriCompliance;
import org.eclipse.jetty.logging.StacklessLogging;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.HttpConfiguration;
@ -2626,6 +2627,11 @@ public class ResourceHandlerTest
assertThat(response.get(LOCATION), endsWith("/context/"));
}
/**
* Tests to attempt to break out of the Context restrictions by
* abusing encoding (or lack thereof), listing output,
* welcome file behaviors, and more.
*/
@ParameterizedTest
@MethodSource("contextBreakoutScenarios")
public void testListingContextBreakout(ResourceHandlerTest.Scenario scenario) throws Exception
@ -2784,9 +2790,27 @@ public class ResourceHandlerTest
assertThat(body, containsString("f??r"));
}
/**
* <p>
* Tests to ensure that when requesting a legit directory listing, you
* cannot arbitrarily include XSS in the output via careful manipulation
* of the request path.
* </p>
* <p>
* This is mainly a test of how the raw request details evolve over time, and
* migrate through the ResourceHandler before it hits the
* ResourceListing.getAsXHTML for output production
* </p>
*/
@Test
public void testListingXSS() throws Exception
{
// Allow unsafe URI requests for this test case specifically
// The requests below abuse the path-param features of URI, and the default UriCompliance mode
// will prevent the use those requests as a 400 Bad Request: Ambiguous URI empty segment
HttpConfiguration httpConfiguration = _local.getConnectionFactory(HttpConfiguration.ConnectionFactory.class).getHttpConfiguration();
httpConfiguration.setUriCompliance(UriCompliance.UNSAFE);
/* create some content in the docroot */
Path one = docRoot.resolve("one");
FS.ensureDirExists(one);
@ -2798,7 +2822,9 @@ public class ResourceHandlerTest
/*
* Intentionally bad request URI. Sending a non-encoded URI with typically
* encoded characters '<', '>', and '"'.
* encoded characters '<', '>', and '"', using the path-param feature of the
* URI spec to still produce a listing. This path-param value should not make it
* down to the ResourceListing.getAsXHTML() method.
*/
String req1 = """
GET /context/;<script>window.alert("hi");</script> HTTP/1.1\r

View File

@ -1,4 +1,3 @@
# Jetty Logging using jetty-slf4j-impl
#org.eclipse.jetty.LEVEL=DEBUG
#org.eclipse.jetty.util.LEVEL=DEBUG
#org.eclipse.jetty.io.LEVEL=DEBUG

View File

@ -16,22 +16,6 @@
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<org.eclipse.jetty.test.client.transport.H3.enable>${h3.test.enabled}</org.eclipse.jetty.test.client.transport.H3.enable>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
<dependencies>
<dependency>
<groupId>org.eclipse.jetty</groupId>

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.test.client.transport;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
@ -67,30 +68,24 @@ public class AbstractTest
protected HttpClient client;
protected Path unixDomainPath;
private static EnumSet<Transport> allTransports()
public static Collection<Transport> transports()
{
EnumSet<Transport> transports = EnumSet.allOf(Transport.class);
// Disable H3 tests unless explicitly enabled with a system property.
if (!Boolean.getBoolean("org.eclipse.jetty.test.client.transport.H3.enable"))
if ("ci".equals(System.getProperty("env")))
transports.remove(Transport.H3);
return transports;
}
public static List<Transport> transports()
public static Collection<Transport> transportsNoFCGI()
{
return List.copyOf(allTransports());
}
public static List<Transport> transportsNoFCGI()
{
EnumSet<Transport> transports = allTransports();
Collection<Transport> transports = transports();
transports.remove(Transport.FCGI);
return List.copyOf(transports);
return transports;
}
public static List<Transport> transportsNoUnixDomain()
public static Collection<Transport> transportsNoUnixDomain()
{
EnumSet<Transport> transports = allTransports();
Collection<Transport> transports = transports();
transports.remove(Transport.UNIX_DOMAIN);
return List.copyOf(transports);
}

View File

@ -1,19 +0,0 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.test.client.transport;
// TODO: write 100 Continue tests for Handler (not Servlet) functionality.
public class Continue100Test
{
}

View File

@ -0,0 +1,127 @@
//
// ========================================================================
// Copyright (c) 1995-2022 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.test.client.transport;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.util.StringRequestContent;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.io.Content;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.util.Callback;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class HttpInterimResponseTest extends AbstractTest
{
@ParameterizedTest
@MethodSource("transportsNoFCGI")
public void testImplicit100Continue(Transport transport) throws Exception
{
start(transport, new Handler.Processor()
{
@Override
public void process(Request request, Response response, Callback callback) throws Exception
{
// Reading the request content immediately
// issues an implicit 100 Continue response.
Content.Source.consumeAll(request, callback);
}
});
ContentResponse response = client.newRequest(newURI(transport))
.headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE))
.body(new StringRequestContent("request-content"))
.timeout(5, TimeUnit.SECONDS)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
}
@ParameterizedTest
@MethodSource("transportsNoFCGI")
public void testMultipleDifferentInterimResponses(Transport transport) throws Exception
{
start(transport, new Handler.Processor()
{
@Override
public void process(Request request, Response response, Callback callback) throws Exception
{
CompletableFuture<Void> completable = response.writeInterim(HttpStatus.CONTINUE_100, HttpFields.EMPTY)
.thenCompose(ignored -> Callback.Completable.with(c -> Content.Source.consumeAll(request, c)))
.thenCompose(ignored ->
{
HttpFields.Mutable fields1 = HttpFields.build();
fields1.put("Progress", "25%");
return response.writeInterim(HttpStatus.PROCESSING_102, fields1);
})
.thenCompose(ignored ->
{
HttpFields.Mutable fields2 = HttpFields.build();
fields2.put("Progress", "100%");
return response.writeInterim(HttpStatus.PROCESSING_102, fields2);
})
.thenRun(() ->
{
response.setStatus(200);
response.getHeaders().put("X-Header", "X-Value");
})
.thenCompose(ignored ->
{
HttpFields.Mutable hints = HttpFields.build();
hints.put(HttpHeader.LINK, "</main.css>; rel=preload");
return response.writeInterim(HttpStatus.EARLY_HINT_103, hints)
.thenApply(i -> hints);
})
.thenCompose(hints1 ->
{
HttpFields.Mutable hints = HttpFields.build();
hints.put(HttpHeader.LINK, "</style.css>; rel=preload");
return response.writeInterim(HttpStatus.EARLY_HINT_103, hints)
.thenApply(i -> HttpFields.build(hints1).add(hints));
})
.thenCompose(hints ->
{
response.getHeaders().put("X-Header-Foo", "Foo");
response.getHeaders().add(hints);
return Callback.Completable.with(c -> Content.Sink.write(response, true, "response-content", c));
});
callback.completeWith(completable);
}
});
ContentResponse response = client.newRequest(newURI(transport))
.headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE))
.body(new StringRequestContent("request-content"))
.timeout(5, TimeUnit.SECONDS)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertEquals("X-Value", response.getHeaders().get("X-Header"));
assertEquals("Foo", response.getHeaders().get("X-Header-Foo"));
assertThat(response.getHeaders().getValuesList(HttpHeader.LINK), contains("</main.css>; rel=preload", "</style.css>; rel=preload"));
assertEquals("response-content", response.getContentAsString());
}
}

View File

@ -477,6 +477,20 @@ public interface Callback extends Invocable
*/
class Completable extends CompletableFuture<Void> implements Callback
{
/**
* <p>Creates a new {@code Completable} to be consumed by the given
* {@code consumer}, then returns the newly created {@code Completable}.</p>
*
* @param consumer the code that consumes the newly created {@code Completable}
* @return the newly created {@code Completable}
*/
public static Completable with(Consumer<Completable> consumer)
{
Completable completable = new Completable();
consumer.accept(completable);
return completable;
}
/**
* Creates a completable future given a callback.
*

View File

@ -26,17 +26,6 @@ import org.slf4j.LoggerFactory;
*/
public interface Promise<C>
{
default void completeWith(CompletableFuture<C> cf)
{
cf.whenComplete((c, x) ->
{
if (x == null)
succeeded(c);
else
failed(x);
});
}
/**
* <p>Callback invoked when the operation completes.</p>
*
@ -135,6 +124,21 @@ public interface Promise<C>
*/
class Completable<S> extends CompletableFuture<S> implements Promise<S>
{
/**
* <p>Creates a new {@code Completable} to be consumed by the given
* {@code consumer}, then returns the newly created {@code Completable}.</p>
*
* @param consumer the code that consumes the newly created {@code Completable}
* @return the newly created {@code Completable}
* @param <R> the type of the result
*/
public static <R> Completable<R> with(Consumer<Promise<R>> consumer)
{
Completable<R> completable = new Completable<>();
consumer.accept(completable);
return completable;
}
@Override
public void succeeded(S result)
{

View File

@ -48,79 +48,9 @@ public class PathResource extends Resource
.build();
private final Path path;
private final Path alias;
private final URI uri;
private Path checkAliasPath()
{
Path abs = path;
// TODO: is this a valid shortcut?
// If the path doesn't exist, then there's no alias to reference
if (!Files.exists(path))
return null;
/* Catch situation where the Path class has already normalized
* the URI eg. input path "aa./foo.txt"
* from an #resolve(String) is normalized away during
* the creation of a Path object reference.
* If the URI is different from the Path.toUri() then
* we will just use the original URI to construct the
* alias reference Path.
*
* We use the method `toUri(Path)` here, instead of
* Path.toUri() to ensure that the path contains
* a trailing slash if it's a directory, (something
* not all FileSystems seem to support)
*/
if (!URIUtil.equalsIgnoreEncodings(uri, toUri(path)))
{
try
{
// Use normalized path to get past navigational references like "/bar/../foo/test.txt"
Path ref = Paths.get(uri.normalize());
return ref.toRealPath();
}
catch (IOException ioe)
{
// If the toRealPath() call fails, then let
// the alias checking routines continue on
// to other techniques.
LOG.trace("IGNORED", ioe);
}
}
if (!abs.isAbsolute())
abs = path.toAbsolutePath();
// Any normalization difference means it's an alias,
// and we don't want to bother further to follow
// symlinks as it's an alias anyway.
Path normal = path.normalize();
if (!isSameName(abs, normal))
return normal;
try
{
if (Files.isSymbolicLink(path))
return path.getParent().resolve(Files.readSymbolicLink(path));
if (Files.exists(path))
{
Path real = abs.toRealPath();
if (!isSameName(abs, real))
return real;
}
}
catch (IOException e)
{
LOG.trace("IGNORED", e);
}
catch (Exception e)
{
LOG.warn("bad alias ({} {}) for {}", e.getClass().getName(), e.getMessage(), path);
}
return null;
}
private boolean targetResolved = false;
private Path targetPath;
/**
* Test if the paths are the same name.
@ -235,13 +165,12 @@ public class PathResource extends Resource
this.path = path;
this.uri = uri;
this.alias = checkAliasPath();
}
@Override
public boolean exists()
{
return Files.exists(alias != null ? alias : path);
return Files.exists(targetPath != null ? targetPath : path);
}
@Override
@ -325,9 +254,87 @@ public class PathResource extends Resource
}
@Override
public URI getAlias()
public URI getTargetURI()
{
return this.alias == null ? null : this.alias.toUri();
if (!targetResolved)
{
targetPath = resolveTargetPath();
targetResolved = true;
}
if (targetPath == null)
return null;
return targetPath.toUri();
}
private Path resolveTargetPath()
{
Path abs = path;
// TODO: is this a valid shortcut?
// If the path doesn't exist, then there's no alias to reference
if (!Files.exists(path))
return null;
/* Catch situation where the Path class has already normalized
* the URI eg. input path "aa./foo.txt"
* from an #resolve(String) is normalized away during
* the creation of a Path object reference.
* If the URI is different from the Path.toUri() then
* we will just use the original URI to construct the
* alias reference Path.
*
* We use the method `toUri(Path)` here, instead of
* Path.toUri() to ensure that the path contains
* a trailing slash if it's a directory, (something
* not all FileSystems seem to support)
*/
if (!URIUtil.equalsIgnoreEncodings(uri, toUri(path)))
{
try
{
// Use normalized path to get past navigational references like "/bar/../foo/test.txt"
Path ref = Paths.get(uri.normalize());
return ref.toRealPath();
}
catch (IOException ioe)
{
// If the toRealPath() call fails, then let
// the alias checking routines continue on
// to other techniques.
LOG.trace("IGNORED", ioe);
}
}
if (!abs.isAbsolute())
abs = path.toAbsolutePath();
// Any normalization difference means it's an alias,
// and we don't want to bother further to follow
// symlinks as it's an alias anyway.
Path normal = path.normalize();
if (!isSameName(abs, normal))
return normal;
try
{
if (Files.isSymbolicLink(path))
return path.getParent().resolve(Files.readSymbolicLink(path));
if (Files.exists(path))
{
Path real = abs.toRealPath();
if (!isSameName(abs, real))
return real;
}
}
catch (IOException e)
{
LOG.trace("IGNORED", e);
}
catch (Exception e)
{
LOG.warn("bad alias ({} {}) for {}", e.getClass().getName(), e.getMessage(), path);
}
return null;
}
@Override

View File

@ -325,15 +325,17 @@ public abstract class Resource implements Iterable<Resource>
*/
public boolean isAlias()
{
return getAlias() != null;
return getTargetURI() != null;
}
/**
* The canonical Alias for the Resource as a URI.
* If this Resource is an alias pointing to a different location,
* return the target location as URI.
*
* @return The canonical Alias of this resource or null if none.
* @return The target URI location of this resource,
* or null if there is no target URI location (eg: not an alias, or a symlink)
*/
public URI getAlias()
public URI getTargetURI()
{
return null;
}

View File

@ -285,7 +285,10 @@ public class ResourceCollection extends Resource
List<Resource> result = new ArrayList<>();
for (Resource r : _resources)
{
result.addAll(r.list());
if (r.isDirectory())
result.addAll(r.list());
else
result.add(r);
}
return result;
}

View File

@ -13,6 +13,7 @@
package org.eclipse.jetty.util.ssl;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@ -53,11 +54,10 @@ public class KeyStoreScanner extends ContainerLifeCycle implements Scanner.Discr
if (Files.isDirectory(monitoredFile))
throw new IllegalArgumentException("expected keystore file not directory");
if (keystoreResource.getAlias() != null)
{
// this resource has an alias, use the alias, as that's what's returned in the Scanner
monitoredFile = Paths.get(keystoreResource.getAlias());
}
// Use real location of keystore (if different), so that change monitoring can work properly
URI targetURI = keystoreResource.getTargetURI();
if (targetURI != null)
monitoredFile = Paths.get(targetURI);
keystoreFile = monitoredFile;
if (LOG.isDebugEnabled())

View File

@ -84,7 +84,7 @@ public class FileSystemResourceTest
assertThat(FileSystemPool.INSTANCE.mounts(), empty());
}
private Matcher<Resource> hasNoAlias()
private Matcher<Resource> hasNoTargetURI()
{
return new BaseMatcher<>()
{
@ -92,24 +92,24 @@ public class FileSystemResourceTest
public boolean matches(Object item)
{
final Resource res = (Resource)item;
return !res.isAlias();
return res.getTargetURI() == null;
}
@Override
public void describeTo(Description description)
{
description.appendText("getAlias should return null");
description.appendText("getTargetURI should return null");
}
@Override
public void describeMismatch(Object item, Description description)
{
description.appendText("was ").appendValue(((Resource)item).getAlias());
description.appendText("was ").appendValue(((Resource)item).getTargetURI());
}
};
}
private Matcher<Resource> isAliasFor(final Resource resource)
private Matcher<Resource> isTargetFor(final Resource resource)
{
return new BaseMatcher<>()
{
@ -117,10 +117,10 @@ public class FileSystemResourceTest
public boolean matches(Object item)
{
final Resource ritem = (Resource)item;
final URI alias = ritem.getAlias();
final URI alias = ritem.getTargetURI();
if (alias == null)
{
return resource.getAlias() == null;
return resource.getTargetURI() == null;
}
else
{
@ -131,13 +131,13 @@ public class FileSystemResourceTest
@Override
public void describeTo(Description description)
{
description.appendText("getAlias should return ").appendValue(resource.getURI());
description.appendText("getTargetURI should return ").appendValue(resource.getURI());
}
@Override
public void describeMismatch(Object item, Description description)
{
description.appendText("was ").appendValue(((Resource)item).getAlias());
description.appendText("was ").appendValue(((Resource)item).getTargetURI());
}
};
}
@ -583,13 +583,13 @@ public class FileSystemResourceTest
// Access to the same resource, but via a symlink means that they are not equivalent
assertThat("foo.equals(bar)", resFoo.equals(resBar), is(false));
assertThat("resource.alias", resFoo, hasNoAlias());
assertThat("resource.uri.alias", ResourceFactory.root().newResource(resFoo.getURI()), hasNoAlias());
assertThat("resource.file.alias", ResourceFactory.root().newResource(resFoo.getPath()), hasNoAlias());
assertThat("resource.targetURI", resFoo, hasNoTargetURI());
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resFoo.getURI()), hasNoTargetURI());
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resFoo.getPath()), hasNoTargetURI());
assertThat("alias", resBar, isAliasFor(resFoo));
assertThat("uri.alias", ResourceFactory.root().newResource(resBar.getURI()), isAliasFor(resFoo));
assertThat("file.alias", ResourceFactory.root().newResource(resBar.getPath()), isAliasFor(resFoo));
assertThat("targetURI", resBar, isTargetFor(resFoo));
assertThat("uri.targetURI", ResourceFactory.root().newResource(resBar.getURI()), isTargetFor(resFoo));
assertThat("file.targetURI", ResourceFactory.root().newResource(resBar.getPath()), isTargetFor(resFoo));
}
@Test
@ -623,13 +623,13 @@ public class FileSystemResourceTest
// Access to the same resource, but via a symlink means that they are not equivalent
assertThat("foo.equals(bar)", resFoo.equals(resBar), is(false));
assertThat("resource.alias", resFoo, hasNoAlias());
assertThat("resource.uri.alias", ResourceFactory.root().newResource(resFoo.getURI()), hasNoAlias());
assertThat("resource.file.alias", ResourceFactory.root().newResource(resFoo.getPath()), hasNoAlias());
assertThat("resource.targetURI", resFoo, hasNoTargetURI());
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resFoo.getURI()), hasNoTargetURI());
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resFoo.getPath()), hasNoTargetURI());
assertThat("alias", resBar, isAliasFor(resFoo));
assertThat("uri.alias", ResourceFactory.root().newResource(resBar.getURI()), isAliasFor(resFoo));
assertThat("file.alias", ResourceFactory.root().newResource(resBar.getPath()), isAliasFor(resFoo));
assertThat("targetURI", resBar, isTargetFor(resFoo));
assertThat("uri.targetURI", ResourceFactory.root().newResource(resBar.getURI()), isTargetFor(resFoo));
assertThat("file.targetURI", ResourceFactory.root().newResource(resBar.getPath()), isTargetFor(resFoo));
}
@Test
@ -644,9 +644,9 @@ public class FileSystemResourceTest
// Reference to actual resource that exists
Resource resource = base.resolve("file");
assertThat("resource.alias", resource, hasNoAlias());
assertThat("resource.uri.alias", ResourceFactory.root().newResource(resource.getURI()), hasNoAlias());
assertThat("resource.file.alias", ResourceFactory.root().newResource(resource.getPath()), hasNoAlias());
assertThat("resource.targetURI", resource, hasNoTargetURI());
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), hasNoTargetURI());
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), hasNoTargetURI());
// On some case insensitive file systems, lets see if an alternate
// case for the filename results in an alias reference
@ -654,9 +654,9 @@ public class FileSystemResourceTest
if (alias.exists())
{
// If it exists, it must be an alias
assertThat("alias", alias, isAliasFor(resource));
assertThat("alias.uri", ResourceFactory.root().newResource(alias.getURI()), isAliasFor(resource));
assertThat("alias.file", ResourceFactory.root().newResource(alias.getPath()), isAliasFor(resource));
assertThat("targetURI", alias, isTargetFor(resource));
assertThat("targetURI.uri", ResourceFactory.root().newResource(alias.getURI()), isTargetFor(resource));
assertThat("targetURI.file", ResourceFactory.root().newResource(alias.getPath()), isTargetFor(resource));
}
}
@ -682,9 +682,9 @@ public class FileSystemResourceTest
// Long filename
Resource resource = base.resolve("TextFile.Long.txt");
assertThat("resource.alias", resource, hasNoAlias());
assertThat("resource.uri.alias", ResourceFactory.root().newResource(resource.getURI()), hasNoAlias());
assertThat("resource.file.alias", ResourceFactory.root().newResource(resource.getPath()), hasNoAlias());
assertThat("resource.targetURI", resource, hasNoTargetURI());
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), hasNoTargetURI());
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), hasNoTargetURI());
// On some versions of Windows, the long filename can be referenced
// via a short 8.3 equivalent filename.
@ -692,9 +692,9 @@ public class FileSystemResourceTest
if (alias.exists())
{
// If it exists, it must be an alias
assertThat("alias", alias, isAliasFor(resource));
assertThat("alias.uri", ResourceFactory.root().newResource(alias.getURI()), isAliasFor(resource));
assertThat("alias.file", ResourceFactory.root().newResource(alias.getPath()), isAliasFor(resource));
assertThat("targetURI", alias, isTargetFor(resource));
assertThat("targetURI.uri", ResourceFactory.root().newResource(alias.getURI()), isTargetFor(resource));
assertThat("targetURI.file", ResourceFactory.root().newResource(alias.getPath()), isTargetFor(resource));
}
}
@ -718,9 +718,9 @@ public class FileSystemResourceTest
Resource base = ResourceFactory.root().newResource(dir);
Resource resource = base.resolve("testfile");
assertThat("resource.alias", resource, hasNoAlias());
assertThat("resource.uri.alias", ResourceFactory.root().newResource(resource.getURI()), hasNoAlias());
assertThat("resource.file.alias", ResourceFactory.root().newResource(resource.getPath()), hasNoAlias());
assertThat("resource.targetURI", resource, hasNoTargetURI());
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), hasNoTargetURI());
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), hasNoTargetURI());
try
{
@ -729,9 +729,9 @@ public class FileSystemResourceTest
if (alias.exists())
{
// If it exists, it must be an alias
assertThat("resource.alias", alias, isAliasFor(resource));
assertThat("resource.uri.alias", ResourceFactory.root().newResource(alias.getURI()), isAliasFor(resource));
assertThat("resource.file.alias", ResourceFactory.root().newResource(alias.getPath()), isAliasFor(resource));
assertThat("resource.targetURI", alias, isTargetFor(resource));
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(alias.getURI()), isTargetFor(resource));
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(alias.getPath()), isTargetFor(resource));
}
}
catch (InvalidPathException e)
@ -760,9 +760,9 @@ public class FileSystemResourceTest
Resource base = ResourceFactory.root().newResource(dir);
Resource resource = base.resolve("testfile");
assertThat("resource.alias", resource, hasNoAlias());
assertThat("resource.uri.alias", ResourceFactory.root().newResource(resource.getURI()), hasNoAlias());
assertThat("resource.file.alias", ResourceFactory.root().newResource(resource.getPath()), hasNoAlias());
assertThat("resource.targetURI", resource, hasNoTargetURI());
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), hasNoTargetURI());
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), hasNoTargetURI());
try
{
@ -773,9 +773,9 @@ public class FileSystemResourceTest
assumeTrue(alias.getURI().getScheme().equals("file"));
// If it exists, it must be an alias
assertThat("resource.alias", alias, isAliasFor(resource));
assertThat("resource.uri.alias", ResourceFactory.root().newResource(alias.getURI()), isAliasFor(resource));
assertThat("resource.file.alias", ResourceFactory.root().newResource(alias.getPath()), isAliasFor(resource));
assertThat("resource.targetURI", alias, isTargetFor(resource));
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(alias.getURI()), isTargetFor(resource));
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(alias.getPath()), isTargetFor(resource));
}
}
catch (InvalidPathException e)
@ -804,9 +804,9 @@ public class FileSystemResourceTest
Resource base = ResourceFactory.root().newResource(dir);
Resource resource = base.resolve("testfile");
assertThat("resource.alias", resource, hasNoAlias());
assertThat("resource.uri.alias", ResourceFactory.root().newResource(resource.getURI()), hasNoAlias());
assertThat("resource.file.alias", ResourceFactory.root().newResource(resource.getPath()), hasNoAlias());
assertThat("resource.targetURI", resource, hasNoTargetURI());
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(resource.getURI()), hasNoTargetURI());
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(resource.getPath()), hasNoTargetURI());
try
{
@ -815,9 +815,9 @@ public class FileSystemResourceTest
if (alias.exists())
{
// If it exists, it must be an alias
assertThat("resource.alias", alias, isAliasFor(resource));
assertThat("resource.uri.alias", ResourceFactory.root().newResource(alias.getURI()), isAliasFor(resource));
assertThat("resource.file.alias", ResourceFactory.root().newResource(alias.getPath()), isAliasFor(resource));
assertThat("resource.targetURI", alias, isTargetFor(resource));
assertThat("resource.uri.targetURI", ResourceFactory.root().newResource(alias.getURI()), isTargetFor(resource));
assertThat("resource.file.targetURI", ResourceFactory.root().newResource(alias.getPath()), isTargetFor(resource));
}
}
catch (InvalidPathException e)
@ -844,7 +844,7 @@ public class FileSystemResourceTest
Resource base = ResourceFactory.root().newResource(dir);
Resource res = base.resolve("foo;");
assertThat("Alias: " + res, res, hasNoAlias());
assertThat("Target URI: " + res, res, hasNoTargetURI());
}
@Test
@ -1039,25 +1039,25 @@ public class FileSystemResourceTest
Resource fileres = ResourceFactory.root().newResource(refQuoted);
assertThat("Exists: " + refQuoted, fileres.exists(), is(true));
assertThat("Alias: " + refQuoted, fileres, hasNoAlias());
assertThat("Target URI: " + refQuoted, fileres, hasNoTargetURI());
URI refEncoded = dir.toUri().resolve("foo%27s.txt");
fileres = ResourceFactory.root().newResource(refEncoded);
assertThat("Exists: " + refEncoded, fileres.exists(), is(true));
assertThat("Alias: " + refEncoded, fileres, hasNoAlias());
assertThat("Target URI: " + refEncoded, fileres, hasNoTargetURI());
URI refQuoteSpace = dir.toUri().resolve("f%20o's.txt");
fileres = ResourceFactory.root().newResource(refQuoteSpace);
assertThat("Exists: " + refQuoteSpace, fileres.exists(), is(true));
assertThat("Alias: " + refQuoteSpace, fileres, hasNoAlias());
assertThat("Target URI: " + refQuoteSpace, fileres, hasNoTargetURI());
URI refEncodedSpace = dir.toUri().resolve("f%20o%27s.txt");
fileres = ResourceFactory.root().newResource(refEncodedSpace);
assertThat("Exists: " + refEncodedSpace, fileres.exists(), is(true));
assertThat("Alias: " + refEncodedSpace, fileres, hasNoAlias());
assertThat("Target URI: " + refEncodedSpace, fileres, hasNoTargetURI());
URI refA = dir.toUri().resolve("foo's.txt");
URI refB = dir.toUri().resolve("foo%27s.txt");
@ -1143,7 +1143,7 @@ public class FileSystemResourceTest
try
{
assertThat("Exists: " + basePath, base.exists(), is(true));
assertThat("Alias: " + basePath, base, hasNoAlias());
assertThat("Target URI: " + basePath, base, hasNoTargetURI());
Resource r = base.resolve("aa%5C/foo.txt");
assertThat("getURI()", r.getPath().toString(), containsString("aa\\/foo.txt"));
@ -1152,8 +1152,8 @@ public class FileSystemResourceTest
if (org.junit.jupiter.api.condition.OS.WINDOWS.isCurrentOs())
{
assertThat("isAlias()", r.isAlias(), is(true));
assertThat("getAlias()", r.getAlias(), notNullValue());
assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("aa/foo.txt"));
assertThat("getTargetURI()", r.getTargetURI(), notNullValue());
assertThat("getTargetURI()", r.getTargetURI().toASCIIString(), containsString("aa/foo.txt"));
assertThat("Exists: " + r, r.exists(), is(true));
}
else
@ -1186,7 +1186,7 @@ public class FileSystemResourceTest
try
{
assertThat("Exists: " + basePath, base.exists(), is(true));
assertThat("Alias: " + basePath, base, hasNoAlias());
assertThat("Target URI: " + basePath, base, hasNoTargetURI());
Resource r = base.resolve("aa./foo.txt");
assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa./foo.txt"));
@ -1194,8 +1194,8 @@ public class FileSystemResourceTest
if (OS.WINDOWS.isCurrentOs())
{
assertThat("isAlias()", r.isAlias(), is(true));
assertThat("getAlias()", r.getAlias(), notNullValue());
assertThat("getAlias()", r.getAlias().toASCIIString(), containsString("aa/foo.txt"));
assertThat("getTargetURI()", r.getTargetURI(), notNullValue());
assertThat("getTargetURI()", r.getTargetURI().toASCIIString(), containsString("aa/foo.txt"));
assertThat("Exists: " + r, r.exists(), is(true));
}
else
@ -1226,7 +1226,7 @@ public class FileSystemResourceTest
try
{
assertThat("Exists: " + basePath, base.exists(), is(true));
assertThat("Alias: " + basePath, base, hasNoAlias());
assertThat("Target URI: " + basePath, base, hasNoTargetURI());
Resource r = base.resolve("/foo.txt");
assertThat("getURI()", r.getURI().toASCIIString(), containsString("/foo.txt"));
@ -1256,13 +1256,13 @@ public class FileSystemResourceTest
try
{
assertThat("Exists: " + basePath, base.exists(), is(true));
assertThat("Alias: " + basePath, base, hasNoAlias());
assertThat("Target URI: " + basePath, base, hasNoTargetURI());
Resource r = base.resolve("//foo.txt");
assertThat("getURI()", r.getURI().toASCIIString(), containsString("/foo.txt"));
assertThat("isAlias()", r.isAlias(), is(false));
assertThat("getAlias()", r.getAlias(), nullValue());
assertThat("getTargetURI()", r.getTargetURI(), nullValue());
}
catch (IllegalArgumentException e)
{
@ -1288,13 +1288,13 @@ public class FileSystemResourceTest
try
{
assertThat("Exists: " + basePath, base.exists(), is(true));
assertThat("Alias: " + basePath, base, hasNoAlias());
assertThat("Target URI: " + basePath, base, hasNoTargetURI());
Resource r = base.resolve("aa//foo.txt");
assertThat("getURI()", r.getURI().toASCIIString(), containsString("aa/foo.txt"));
assertThat("isAlias()", r.isAlias(), is(false));
assertThat("getAlias()", r.getAlias(), nullValue());
assertThat("getTargetURI()", r.getTargetURI(), nullValue());
}
catch (IllegalArgumentException e)
{
@ -1342,11 +1342,11 @@ public class FileSystemResourceTest
Resource base = ResourceFactory.root().newResource(utf8Dir);
assertThat("Exists: " + utf8Dir, base.exists(), is(true));
assertThat("Alias: " + utf8Dir, base, hasNoAlias());
assertThat("Target URI: " + utf8Dir, base, hasNoTargetURI());
Resource r = base.resolve("file.txt");
assertThat("Exists: " + r, r.exists(), is(true));
assertThat("Alias: " + r, r, hasNoAlias());
assertThat("Target URI: " + r, r, hasNoTargetURI());
}
@Test
@ -1357,7 +1357,7 @@ public class FileSystemResourceTest
Resource resource = base.resolve("WEB-INF/");
assertThat("getURI()", resource.getURI().toASCIIString(), containsString("path/WEB-INF/"));
assertThat("isAlias()", resource.isAlias(), is(false));
assertThat("getAlias()", resource.getAlias(), nullValue());
assertThat("getTargetURI()", resource.getTargetURI(), nullValue());
}
private String toString(Resource resource) throws IOException

View File

@ -303,37 +303,37 @@ public class PathResourceTest
// Test not alias paths
Resource resource = resourceFactory.newResource(file);
assertTrue(resource.exists());
assertNull(resource.getAlias());
assertNull(resource.getTargetURI());
resource = resourceFactory.newResource(file.toAbsolutePath());
assertTrue(resource.exists());
assertNull(resource.getAlias());
assertNull(resource.getTargetURI());
resource = resourceFactory.newResource(file.toUri());
assertTrue(resource.exists());
assertNull(resource.getAlias());
assertNull(resource.getTargetURI());
resource = resourceFactory.newResource(file.toUri().toString());
assertTrue(resource.exists());
assertNull(resource.getAlias());
assertNull(resource.getTargetURI());
resource = archiveResource.resolve("test.txt");
assertTrue(resource.exists());
assertNull(resource.getAlias());
assertNull(resource.getTargetURI());
// Test alias paths
resource = resourceFactory.newResource(file0);
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
assertNotNull(resource.getTargetURI());
resource = resourceFactory.newResource(file0.toAbsolutePath());
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
assertNotNull(resource.getTargetURI());
resource = resourceFactory.newResource(file0.toUri());
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
assertNotNull(resource.getTargetURI());
resource = resourceFactory.newResource(file0.toUri().toString());
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
assertNotNull(resource.getTargetURI());
resource = archiveResource.resolve("test.txt\0");
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
assertNotNull(resource.getTargetURI());
}
catch (InvalidPathException e)
{
@ -402,9 +402,9 @@ public class PathResourceTest
assertThat("resource.uri.alias", resourceFactory.newResource(resFoo.getURI()).isAlias(), is(false));
assertThat("resource.file.alias", resourceFactory.newResource(resFoo.getPath()).isAlias(), is(false));
assertThat("alias", resBar.getAlias(), is(resFoo.getURI()));
assertThat("uri.alias", resourceFactory.newResource(resBar.getURI()).getAlias(), is(resFoo.getURI()));
assertThat("file.alias", resourceFactory.newResource(resBar.getPath()).getAlias(), is(resFoo.getURI()));
assertThat("targetURI", resBar.getTargetURI(), is(resFoo.getURI()));
assertThat("uri.targetURI", resourceFactory.newResource(resBar.getURI()).getTargetURI(), is(resFoo.getURI()));
assertThat("file.targetURI", resourceFactory.newResource(resBar.getPath()).getTargetURI(), is(resFoo.getURI()));
}
catch (InvalidPathException e)
{

View File

@ -132,37 +132,37 @@ public class ResourceAliasTest
// Test not alias paths
Resource resource = resourceFactory.newResource(file);
assertTrue(resource.exists());
assertNull(resource.getAlias());
assertNull(resource.getTargetURI());
resource = resourceFactory.newResource(file.toAbsolutePath());
assertTrue(resource.exists());
assertNull(resource.getAlias());
assertNull(resource.getTargetURI());
resource = resourceFactory.newResource(file.toUri());
assertTrue(resource.exists());
assertNull(resource.getAlias());
assertNull(resource.getTargetURI());
resource = resourceFactory.newResource(file.toUri().toString());
assertTrue(resource.exists());
assertNull(resource.getAlias());
assertNull(resource.getTargetURI());
resource = dir.resolve("test.txt");
assertTrue(resource.exists());
assertNull(resource.getAlias());
assertNull(resource.getTargetURI());
// Test alias paths
resource = resourceFactory.newResource(file0);
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
assertNotNull(resource.getTargetURI());
resource = resourceFactory.newResource(file0.toAbsolutePath());
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
assertNotNull(resource.getTargetURI());
resource = resourceFactory.newResource(file0.toUri());
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
assertNotNull(resource.getTargetURI());
resource = resourceFactory.newResource(file0.toUri().toString());
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
assertNotNull(resource.getTargetURI());
resource = dir.resolve("test.txt\0");
assertTrue(resource.exists());
assertNotNull(resource.getAlias());
assertNotNull(resource.getTargetURI());
}
catch (InvalidPathException e)
{

View File

@ -386,7 +386,7 @@ public class ResourceTest
assertNotNull(dot);
assertTrue(dot.exists());
assertTrue(dot.isAlias(), "Reference to '.' is an alias to itself");
assertTrue(Files.isSameFile(dot.getPath(), Paths.get(dot.getAlias())));
assertTrue(Files.isSameFile(dot.getPath(), Paths.get(dot.getTargetURI())));
}
@Test
@ -413,7 +413,7 @@ public class ResourceTest
assertNotNull(dot);
assertTrue(dot.exists());
assertTrue(dot.isAlias(), "Reference to '.' is an alias to itself");
assertTrue(Files.isSameFile(dot.getPath(), Paths.get(dot.getAlias())));
assertTrue(Files.isSameFile(dot.getPath(), Paths.get(dot.getTargetURI())));
}
@Test

View File

@ -23,7 +23,6 @@ import org.eclipse.jetty.ee10.servlet.BaseHolder;
import org.eclipse.jetty.ee10.servlet.Source.Origin;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.ee10.webapp.WebDescriptor;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.util.thread.AutoLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -169,7 +168,7 @@ public class AnnotationIntrospector
String descriptorLocation = holder.getSource().getResource();
if (descriptorLocation == null)
return true; //no descriptor, can't be metadata-complete
return !WebDescriptor.isMetaDataComplete(_context.getMetaData().getFragmentDescriptor(ResourceFactory.of(_context).newResource(descriptorLocation)));
return !WebDescriptor.isMetaDataComplete(_context.getMetaData().getFragmentDescriptor(_context.getResourceFactory().newResource(descriptorLocation)));
}
}
}

View File

@ -21,7 +21,6 @@ import org.eclipse.jetty.ee10.plus.webapp.EnvConfiguration;
import org.eclipse.jetty.ee10.plus.webapp.PlusConfiguration;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.resource.ResourceFactory;
/**
* ServerWithJNDI
@ -37,7 +36,7 @@ public class ServerWithJNDI
WebAppContext webapp = new WebAppContext();
webapp.setContextPath("/");
Path testJndiWar = JettyDemos.find("jetty-ee10-demo-jndi-webapp/target/jetty-ee10-demo-jndi-webapp-@VER@.war");
webapp.setWarResource(ResourceFactory.of(webapp).newResource(testJndiWar));
webapp.setWarResource(webapp.getResourceFactory().newResource(testJndiWar));
server.setHandler(webapp);
// Enable parsing of jndi-related parts of web.xml and jetty-env.xml

View File

@ -115,7 +115,7 @@ public class TestServer
Path webappBase = webappProjectRoot.resolve("jetty-ee10/jetty-ee10-demos/jetty-ee10-demo-jetty-webapp/src/main/webapp");
if (!Files.exists(webappBase))
throw new FileNotFoundException(webappBase.toString());
webapp.setBaseResource(ResourceFactory.of(server).newResource(webappBase));
webapp.setBaseResource(webapp.getResourceFactory().newResource(webappBase));
webapp.setAttribute(MetaInfConfiguration.CONTAINER_JAR_PATTERN,
".*/test-jetty-webapp/target/classes.*$|" +
".*/jakarta.servlet.api-[^/]*\\.jar$|.*/jakarta.servlet.jsp.jstl-.*\\.jar$|.*/org.apache.taglibs.taglibs-standard.*\\.jar$"

View File

@ -21,10 +21,11 @@ import java.util.List;
import java.util.stream.Collectors;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojoExecutionException;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Parameter;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
/**
* Base class for all goals that operate on unassembled webapps.
@ -104,18 +105,25 @@ public abstract class AbstractUnassembledWebAppMojo extends AbstractWebAppMojo
}
@Override
protected void configureWebApp() throws Exception
protected void configureWebApp() throws AbstractMojoExecutionException
{
super.configureWebApp();
configureUnassembledWebApp();
try
{
configureUnassembledWebApp();
}
catch (IOException e)
{
throw new MojoFailureException("Unable to configure unassembled webapp", e);
}
}
/**
* Configure a webapp that has not been assembled into a war.
*
* @throws Exception
* @throws IOException
*/
protected void configureUnassembledWebApp() throws Exception
protected void configureUnassembledWebApp() throws IOException
{
//Set up the location of the webapp.
//There are 2 parts to this: setWar() and setBaseResource(). On standalone jetty,
@ -131,7 +139,7 @@ public abstract class AbstractUnassembledWebAppMojo extends AbstractWebAppMojo
//Use the default static resource location
if (!webAppSourceDirectory.exists())
webAppSourceDirectory.mkdirs();
originalBaseResource = ResourceFactory.of(webApp).newResource(webAppSourceDirectory.getCanonicalPath());
originalBaseResource = webApp.getResourceFactory().newResource(webAppSourceDirectory.getCanonicalPath());
}
else
originalBaseResource = webApp.getBaseResource();
@ -167,7 +175,7 @@ public abstract class AbstractUnassembledWebAppMojo extends AbstractWebAppMojo
//Has an explicit web.xml file been configured to use?
if (webXml != null)
{
Resource r = ResourceFactory.of(webApp).newResource(webXml.toPath());
Resource r = webApp.getResourceFactory().newResource(webXml.toPath());
if (r.exists() && !r.isDirectory())
{
webApp.setDescriptor(r.toString());

View File

@ -39,6 +39,7 @@ import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.model.Dependency;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.AbstractMojoExecutionException;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
@ -781,7 +782,7 @@ public abstract class AbstractWebAppMojo extends AbstractMojo
* @throws Exception
*/
protected void configureWebApp()
throws Exception
throws AbstractMojoExecutionException
{
if (webApp == null)
webApp = new MavenWebAppContext();

View File

@ -14,8 +14,11 @@
package org.eclipse.jetty.ee10.maven.plugin;
import java.io.File;
import java.io.IOException;
import org.apache.maven.plugin.AbstractMojoExecutionException;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
@ -36,21 +39,30 @@ public class JettyEffectiveWebXml extends AbstractUnassembledWebAppMojo
protected File effectiveWebXml;
@Override
public void configureWebApp() throws Exception
public void configureWebApp() throws AbstractMojoExecutionException
{
//Use a nominated war file for which to generate the effective web.xml, or
//if that is not set, try to use the details of the current project's
//unassembled webapp
super.configureWebApp();
if (StringUtil.isBlank(webApp.getWar()))
try
{
if (StringUtil.isBlank(webApp.getWar()))
{
super.configureUnassembledWebApp();
}
}
catch (IOException e)
{
throw new MojoFailureException("Unable to configure unassembled webapp", e);
}
}
/**
* Override so we can call the parent's method in a different order.
*/
@Override
protected void configureUnassembledWebApp() throws Exception
protected void configureUnassembledWebApp()
{
}
@ -76,7 +88,7 @@ public class JettyEffectiveWebXml extends AbstractUnassembledWebAppMojo
{
try
{
QuickStartGenerator generator = new QuickStartGenerator(effectiveWebXml, webApp);
QuickStartGenerator generator = new QuickStartGenerator(effectiveWebXml.toPath(), webApp);
generator.generate();
}
catch (Exception e)

View File

@ -29,7 +29,6 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ShutdownMonitor;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.resource.ResourceFactory;
/**
* JettyEmbedded
@ -185,7 +184,7 @@ public class JettyEmbedder extends AbstractLifeCycle
this.stopKey = stopKey;
}
public void setWebApp(MavenWebAppContext app) throws Exception
public void setWebApp(MavenWebAppContext app)
{
webApp = app;
}
@ -280,7 +279,7 @@ public class JettyEmbedder extends AbstractLifeCycle
Path qs = webApp.getTempDirectory().toPath().resolve("quickstart-web.xml");
if (Files.exists(qs) && Files.isRegularFile(qs))
{
webApp.setAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML, ResourceFactory.of(webApp).newResource(qs));
webApp.setAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML, qs);
webApp.addConfiguration(new MavenQuickStartConfiguration());
webApp.setAttribute(QuickStartConfiguration.MODE, Mode.QUICKSTART);
}

View File

@ -15,7 +15,6 @@ package org.eclipse.jetty.ee10.maven.plugin;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
@ -51,10 +50,10 @@ public class JettyForkedChild extends ContainerLifeCycle
/**
* @param args arguments that were passed to main
* @throws Exception
* @throws IOException if unable to configure
*/
public JettyForkedChild(String[] args)
throws Exception
throws IOException
{
jetty = new JettyEmbedder();
configure(args);
@ -64,10 +63,10 @@ public class JettyForkedChild extends ContainerLifeCycle
* Based on the args passed to the program, configure jetty.
*
* @param args args that were passed to the program.
* @throws Exception
* @throws IOException if unable to load webprops
*/
public void configure(String[] args)
throws Exception
throws IOException
{
Map<String, String> jettyProperties = new HashMap<>();
@ -171,10 +170,9 @@ public class JettyForkedChild extends ContainerLifeCycle
* present.
*
* @return file contents as properties
* @throws FileNotFoundException
* @throws IOException
*/
private Properties loadWebAppProps() throws FileNotFoundException, IOException
private Properties loadWebAppProps() throws IOException
{
Properties props = new Properties();
if (Objects.nonNull(webAppPropsFile))

View File

@ -112,9 +112,9 @@ public class JettyForker extends AbstractForker
throws Exception
{
//Run the webapp to create the quickstart file and properties file
generator = new QuickStartGenerator(forkWebXml, webApp);
generator = new QuickStartGenerator(forkWebXml.toPath(), webApp);
generator.setContextXml(contextXml);
generator.setWebAppPropsFile(webAppPropsFile);
generator.setWebAppProps(webAppPropsFile.toPath());
generator.setServer(server);
generator.generate();

View File

@ -32,7 +32,6 @@ import org.eclipse.jetty.util.IncludeExcludeSet;
import org.eclipse.jetty.util.Scanner;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.util.thread.Scheduler;
/**
@ -220,7 +219,7 @@ public class JettyRunMojo extends AbstractUnassembledWebAppMojo
{
if (webApp.getDescriptor() != null)
{
Resource r = ResourceFactory.of(webApp).newResource(webApp.getDescriptor());
Resource r = webApp.getResourceFactory().newResource(webApp.getDescriptor());
scanner.addFile(r.getPath());
}

View File

@ -19,6 +19,7 @@ import java.nio.file.Paths;
import java.util.Date;
import java.util.Set;
import org.apache.maven.plugin.AbstractMojoExecutionException;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Execute;
import org.apache.maven.plugins.annotations.LifecyclePhase;
@ -67,7 +68,7 @@ public class JettyRunWarMojo extends AbstractWebAppMojo
protected Path war;
@Override
public void configureWebApp() throws Exception
public void configureWebApp() throws AbstractMojoExecutionException
{
super.configureWebApp();
//if no war has been explicitly configured, use the one from the webapp project

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.ee10.maven.plugin;
import java.io.File;
import java.nio.file.Path;
import org.apache.maven.plugin.AbstractMojoExecutionException;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
@ -59,7 +60,7 @@ public class JettyStartWarMojo extends AbstractWebAppMojo
protected JettyHomeForker homeForker;
@Override
public void configureWebApp() throws Exception
public void configureWebApp() throws AbstractMojoExecutionException
{
super.configureWebApp();
//if a war has not been explicitly configured, use the one from the project

View File

@ -22,7 +22,6 @@ import org.eclipse.jetty.ee10.webapp.MetaInfConfiguration;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.util.FileID;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -68,7 +67,7 @@ public class MavenMetaInfConfiguration extends MetaInfConfiguration
try
{
LOG.debug(" add resource to resources to examine {}", file);
list.add(ResourceFactory.of(context).newResource(file.toURI()));
list.add(context.getResourceFactory().newResource(file.toURI()));
}
catch (Exception e)
{
@ -102,7 +101,7 @@ public class MavenMetaInfConfiguration extends MetaInfConfiguration
{
try
{
list.add(ResourceFactory.of(context).newResource(file.toURI()));
list.add(context.getResourceFactory().newResource(file.toURI()));
}
catch (Exception e)
{

View File

@ -41,7 +41,6 @@ import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -100,7 +99,7 @@ public class MavenWebAppContext extends WebAppContext
*/
private boolean _baseAppFirst = true;
public MavenWebAppContext() throws Exception
public MavenWebAppContext()
{
super();
// Turn off copyWebInf option as it is not applicable for plugin.
@ -232,7 +231,7 @@ public class MavenWebAppContext extends WebAppContext
.map(URI::create)
.toList();
setBaseResource(ResourceFactory.of(this).newResource(uris));
setBaseResource(this.getResourceFactory().newResource(uris));
}
catch (Throwable t)
{
@ -319,7 +318,7 @@ public class MavenWebAppContext extends WebAppContext
for (Configuration c : configurations)
{
if (c instanceof EnvConfiguration)
((EnvConfiguration)c).setJettyEnvResource(ResourceFactory.of(this).newResource(getJettyEnvXml()));
((EnvConfiguration)c).setJettyEnvResource(this.getResourceFactory().newResource(getJettyEnvXml()));
}
}
@ -383,9 +382,9 @@ public class MavenWebAppContext extends WebAppContext
// return the resource matching the web-inf classes
// rather than the test classes
if (_classes != null)
return ResourceFactory.of(this).newResource(_classes.toPath());
return this.getResourceFactory().newResource(_classes.toPath());
else if (_testClasses != null)
return ResourceFactory.of(this).newResource(_testClasses.toPath());
return this.getResourceFactory().newResource(_testClasses.toPath());
}
else
{
@ -395,7 +394,7 @@ public class MavenWebAppContext extends WebAppContext
while (res == null && (i < _webInfClasses.size()))
{
String newPath = StringUtil.replace(uri, WEB_INF_CLASSES_PREFIX, _webInfClasses.get(i).getPath());
res = ResourceFactory.of(this).newResource(newPath);
res = this.getResourceFactory().newResource(newPath);
if (!res.exists())
{
res = null;
@ -416,7 +415,7 @@ public class MavenWebAppContext extends WebAppContext
return null;
File jarFile = _webInfJarMap.get(jarName);
if (jarFile != null)
return ResourceFactory.of(this).newResource(jarFile.getPath());
return this.getResourceFactory().newResource(jarFile.getPath());
return null;
}

View File

@ -39,8 +39,7 @@ public class OverlayManager
this.warPlugin = warPlugin;
}
public void applyOverlays(MavenWebAppContext webApp)
throws Exception
public void applyOverlays(MavenWebAppContext webApp) throws IOException
{
List<Resource> resourceBases = new ArrayList<Resource>();
@ -71,8 +70,7 @@ public class OverlayManager
* Generate an ordered list of overlays
*/
protected List<Overlay> getOverlays()
throws Exception
{
{
Set<Artifact> matchedWarArtifacts = new HashSet<Artifact>();
List<Overlay> overlays = new ArrayList<Overlay>();
@ -127,7 +125,7 @@ public class OverlayManager
*/
protected Resource unpackOverlay(Overlay overlay)
throws IOException
{
{
if (overlay.getResource() == null)
return null; //nothing to unpack

View File

@ -13,13 +13,12 @@
package org.eclipse.jetty.ee10.maven.plugin;
import java.io.File;
import java.nio.file.Path;
import org.eclipse.jetty.ee10.annotations.AnnotationConfiguration;
import org.eclipse.jetty.ee10.quickstart.QuickStartConfiguration;
import org.eclipse.jetty.ee10.quickstart.QuickStartConfiguration.Mode;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
/**
@ -31,9 +30,9 @@ import org.eclipse.jetty.util.thread.QueuedThreadPool;
*/
public class QuickStartGenerator
{
private File quickstartXml;
private MavenWebAppContext webApp;
private File webAppPropsFile;
private final Path quickstartXml;
private final MavenWebAppContext webApp;
private Path webAppProps;
private String contextXml;
private boolean prepared = false;
private Server server;
@ -43,10 +42,10 @@ public class QuickStartGenerator
* @param quickstartXml the file to generate quickstart into
* @param webApp the webapp for which to generate quickstart
*/
public QuickStartGenerator(File quickstartXml, MavenWebAppContext webApp)
public QuickStartGenerator(Path quickstartXml, MavenWebAppContext webApp)
{
this.quickstartXml = quickstartXml;
this.webApp = webApp;
this.webApp = webApp == null ? new MavenWebAppContext() : webApp;
}
/**
@ -60,7 +59,7 @@ public class QuickStartGenerator
/**
* @return the quickstartXml
*/
public File getQuickstartXml()
public Path getQuickstartXml()
{
return quickstartXml;
}
@ -81,17 +80,17 @@ public class QuickStartGenerator
this.server = server;
}
public File getWebAppPropsFile()
public Path getWebAppProps()
{
return webAppPropsFile;
return webAppProps;
}
/**
* @param webAppPropsFile properties file describing the webapp
* @param webAppProps properties file describing the webapp
*/
public void setWebAppPropsFile(File webAppPropsFile)
public void setWebAppProps(Path webAppProps)
{
this.webAppPropsFile = webAppPropsFile;
this.webAppProps = webAppProps;
}
public String getContextXml()
@ -109,19 +108,13 @@ public class QuickStartGenerator
/**
* Configure the webapp in preparation for quickstart generation.
*
* @throws Exception
*/
private void prepareWebApp()
throws Exception
{
if (webApp == null)
webApp = new MavenWebAppContext();
//set the webapp up to do very little other than generate the quickstart-web.xml
webApp.addConfiguration(new MavenQuickStartConfiguration());
webApp.setAttribute(QuickStartConfiguration.MODE, Mode.GENERATE);
webApp.setAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML, ResourceFactory.of(webApp).newResource(quickstartXml.toPath()));
webApp.setAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML, quickstartXml);
webApp.setAttribute(QuickStartConfiguration.ORIGIN_ATTRIBUTE, "o");
webApp.setCopyWebDir(false);
webApp.setCopyWebInf(false);
@ -133,8 +126,7 @@ public class QuickStartGenerator
*
* @throws Exception
*/
public void generate()
throws Exception
public void generate() throws Exception
{
if (quickstartXml == null)
throw new IllegalStateException("No quickstart xml output file");
@ -174,8 +166,8 @@ public class QuickStartGenerator
webApp.start(); //just enough to generate the quickstart
//save config of the webapp BEFORE we stop
if (webAppPropsFile != null)
WebAppPropertyConverter.toProperties(webApp, webAppPropsFile, contextXml);
if (webAppProps != null)
WebAppPropertyConverter.toProperties(webApp, webAppProps.toFile(), contextXml);
}
finally
{

View File

@ -54,9 +54,8 @@ public class ServerSupport
* @param server the server to use
* @param contextHandlers the context handlers to include
* @param requestLog a request log to use
* @throws Exception
*/
public static void configureHandlers(Server server, List<ContextHandler> contextHandlers, RequestLog requestLog) throws Exception
public static void configureHandlers(Server server, List<ContextHandler> contextHandlers, RequestLog requestLog)
{
if (server == null)
throw new IllegalArgumentException("Server is null");
@ -148,9 +147,8 @@ public class ServerSupport
* Add a WebAppContext to a Server
* @param server the server to use
* @param webapp the webapp to add
* @throws Exception
*/
public static void addWebApplication(Server server, WebAppContext webapp) throws Exception
public static void addWebApplication(Server server, WebAppContext webapp)
{
if (server == null)
throw new IllegalArgumentException("Server is null");

View File

@ -15,6 +15,7 @@ package org.eclipse.jetty.ee10.maven.plugin;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
@ -31,7 +32,6 @@ import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.xml.XmlConfiguration;
/**
@ -64,10 +64,10 @@ public class WebAppPropertyConverter
* @param webApp the webapp to convert
* @param propsFile the file to put the properties into
* @param contextXml the optional context xml file related to the webApp
* @throws Exception if any I/O exception occurs
* @throws IOException if any I/O exception occurs
*/
public static void toProperties(MavenWebAppContext webApp, File propsFile, String contextXml)
throws Exception
throws IOException
{
if (webApp == null)
throw new IllegalArgumentException("No webapp");
@ -174,7 +174,7 @@ public class WebAppPropertyConverter
if (resource == null)
throw new IllegalStateException("No resource");
fromProperties(webApp, ResourceFactory.of(webApp).newResource(resource).getPath(), server, jettyProperties);
fromProperties(webApp, webApp.getResourceFactory().newResource(resource).getPath(), server, jettyProperties);
}
/**
@ -209,7 +209,7 @@ public class WebAppPropertyConverter
str = webAppProperties.getProperty(QUICKSTART_WEB_XML);
if (!StringUtil.isBlank(str))
{
webApp.setAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML, ResourceFactory.of(webApp).newResource(str));
webApp.setAttribute(QuickStartConfiguration.QUICKSTART_WEB_XML, webApp.getResourceFactory().newResource(str));
}
// - the tmp directory
@ -228,7 +228,7 @@ public class WebAppPropertyConverter
// This is a use provided list of overlays, which could have mountable entries.
List<URI> uris = URIUtil.split(str);
webApp.setWar(null);
webApp.setBaseResource(ResourceFactory.of(webApp).newResource(uris));
webApp.setBaseResource(webApp.getResourceFactory().newResource(uris));
}
str = webAppProperties.getProperty(WAR_FILE);
@ -290,7 +290,7 @@ public class WebAppPropertyConverter
str = (String)webAppProperties.getProperty(CONTEXT_XML);
if (!StringUtil.isBlank(str))
{
XmlConfiguration xmlConfiguration = new XmlConfiguration(ResourceFactory.of(webApp).newResource(str));
XmlConfiguration xmlConfiguration = new XmlConfiguration(webApp.getResourceFactory().newResource(str));
xmlConfiguration.getIdMap().put("Server", server);
//add in any properties
if (jettyProperties != null)

View File

@ -13,12 +13,17 @@
package org.eclipse.jetty.ee10.maven.plugin;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.toolchain.test.jupiter.WorkDir;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
@ -27,24 +32,32 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
*/
public class TestQuickStartGenerator
{
public WorkDir workDir;
@Test
public void testGenerator() throws Exception
{
Path tmpDir = workDir.getEmptyPathDir();
MavenWebAppContext webApp = new MavenWebAppContext();
webApp.setContextPath("/shouldbeoverridden");
webApp.setBaseResource(MavenTestingUtils.getTestResourcePathDir("root"));
File quickstartFile = new File(MavenTestingUtils.getTargetTestingDir(), "quickstart-web.xml");
Path rootDir = MavenTestingUtils.getTargetPath("test-classes/root");
assertTrue(Files.exists(rootDir));
assertTrue(Files.isDirectory(rootDir));
webApp.setBaseResource(ResourceFactory.root().newResource(rootDir));
Path quickstartFile = tmpDir.resolve("quickstart-web.xml");
QuickStartGenerator generator = new QuickStartGenerator(quickstartFile, webApp);
generator.setContextXml(MavenTestingUtils.getTestResourceFile("embedder-context.xml").getAbsolutePath());
generator.setContextXml(MavenTestingUtils.getTargetFile("test-classes/embedder-context.xml").getAbsolutePath());
generator.setServer(new Server());
MavenTestingUtils.getTargetTestingDir().mkdirs();
File propsFile = new File(MavenTestingUtils.getTargetTestingDir(), "webapp.props");
propsFile.createNewFile();
generator.setWebAppPropsFile(propsFile);
Path propsFile = tmpDir.resolve("webapp.props");
Files.createFile(propsFile);
generator.setWebAppProps(propsFile);
generator.generate();
assertTrue(propsFile.exists());
assertTrue(propsFile.length() > 0);
assertTrue(quickstartFile.exists());
assertTrue(quickstartFile.length() > 0);
assertTrue(Files.exists(propsFile));
assertThat(Files.size(propsFile), greaterThan(0L));
assertTrue(Files.exists(quickstartFile));
assertThat(Files.size(quickstartFile), greaterThan(0L));
}
}

View File

@ -154,20 +154,20 @@ public class PlusDescriptorProcessorTest
doEnvConfiguration(envCtx, vacuumStringEnvEntry);
URL webXml = Thread.currentThread().getContextClassLoader().getResource("web.xml");
webDescriptor = new WebDescriptor(org.eclipse.jetty.util.resource.ResourceFactory.of(context).newResource(webXml));
webDescriptor = new WebDescriptor(context.getResourceFactory().newResource(webXml));
webDescriptor.parse(WebDescriptor.getParser(false));
URL frag1Xml = Thread.currentThread().getContextClassLoader().getResource("web-fragment-1.xml");
fragDescriptor1 = new FragmentDescriptor(org.eclipse.jetty.util.resource.ResourceFactory.of(context).newResource(frag1Xml));
fragDescriptor1 = new FragmentDescriptor(context.getResourceFactory().newResource(frag1Xml));
fragDescriptor1.parse(WebDescriptor.getParser(false));
URL frag2Xml = Thread.currentThread().getContextClassLoader().getResource("web-fragment-2.xml");
fragDescriptor2 = new FragmentDescriptor(org.eclipse.jetty.util.resource.ResourceFactory.of(context).newResource(frag2Xml));
fragDescriptor2 = new FragmentDescriptor(context.getResourceFactory().newResource(frag2Xml));
fragDescriptor2.parse(WebDescriptor.getParser(false));
URL frag3Xml = Thread.currentThread().getContextClassLoader().getResource("web-fragment-3.xml");
fragDescriptor3 = new FragmentDescriptor(org.eclipse.jetty.util.resource.ResourceFactory.of(context).newResource(frag3Xml));
fragDescriptor3 = new FragmentDescriptor(context.getResourceFactory().newResource(frag3Xml));
fragDescriptor3.parse(WebDescriptor.getParser(false));
URL frag4Xml = Thread.currentThread().getContextClassLoader().getResource("web-fragment-4.xml");
fragDescriptor4 = new FragmentDescriptor(org.eclipse.jetty.util.resource.ResourceFactory.of(context).newResource(frag4Xml));
fragDescriptor4 = new FragmentDescriptor(context.getResourceFactory().newResource(frag4Xml));
fragDescriptor4.parse(WebDescriptor.getParser(false));
Thread.currentThread().setContextClassLoader(oldLoader);
}

View File

@ -181,15 +181,14 @@ public class ConnectHandlerSSLTest extends AbstractConnectHandlerTest
else
{
builder.append("\r\n");
Callback.Completable completable = new Callback.Completable();
Content.Sink.write(response, false, builder.toString(), completable);
completable.whenComplete((r, x) ->
{
if (x != null)
callback.failed(x);
else
response.write(true, ByteBuffer.wrap(bytes), callback);
});
Callback.Completable.with(c -> Content.Sink.write(response, false, builder.toString(), c))
.whenComplete((r, x) ->
{
if (x != null)
callback.failed(x);
else
response.write(true, ByteBuffer.wrap(bytes), callback);
});
}
}
else

View File

@ -872,15 +872,14 @@ public class ConnectHandlerTest extends AbstractConnectHandlerTest
else
{
builder.append("\r\n");
Callback.Completable completable = new Callback.Completable();
Content.Sink.write(response, false, builder.toString(), completable);
completable.whenComplete((r, x) ->
{
if (x != null)
callback.failed(x);
else
response.write(true, ByteBuffer.wrap(bytes), callback);
});
Callback.Completable.with(c -> Content.Sink.write(response, false, builder.toString(), c))
.whenComplete((r, x) ->
{
if (x != null)
callback.failed(x);
else
response.write(true, ByteBuffer.wrap(bytes), callback);
});
}
}
case "/close" ->

View File

@ -15,6 +15,7 @@ package org.eclipse.jetty.ee10.quickstart;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;
@ -99,8 +100,9 @@ public class QuickStartConfiguration extends AbstractConfiguration
throw new IllegalStateException("Bad Quickstart location");
//look for quickstart-web.xml in WEB-INF of webapp
Resource quickStartWebXml = getQuickStartWebXml(context);
LOG.debug("quickStartWebXml={} exists={}", quickStartWebXml, quickStartWebXml.exists());
Path quickStartWebXml = getQuickStartWebXml(context);
if (LOG.isDebugEnabled())
LOG.debug("quickStartWebXml={} exists={}", quickStartWebXml, Files.exists(quickStartWebXml));
//Get the mode
Object o = context.getAttribute(MODE);
@ -114,7 +116,7 @@ public class QuickStartConfiguration extends AbstractConfiguration
{
case GENERATE:
{
if (quickStartWebXml.exists())
if (Files.exists(quickStartWebXml))
LOG.info("Regenerating {}", quickStartWebXml);
else
LOG.info("Generating {}", quickStartWebXml);
@ -128,7 +130,7 @@ public class QuickStartConfiguration extends AbstractConfiguration
}
case AUTO:
{
if (quickStartWebXml.exists())
if (Files.exists(quickStartWebXml))
{
quickStart(context);
}
@ -141,7 +143,7 @@ public class QuickStartConfiguration extends AbstractConfiguration
break;
}
case QUICKSTART:
if (quickStartWebXml.exists())
if (Files.exists(quickStartWebXml))
quickStart(context);
else
throw new IllegalStateException("No " + quickStartWebXml);
@ -159,7 +161,8 @@ public class QuickStartConfiguration extends AbstractConfiguration
if (attr != null)
generator.setOriginAttribute(attr.toString());
generator.setQuickStartWebXml((Resource)context.getAttribute(QUICKSTART_WEB_XML));
Path quickStartWebXml = getQuickStartWebXml(context);
generator.setQuickStartWebXml(quickStartWebXml);
}
@Override
@ -181,7 +184,8 @@ public class QuickStartConfiguration extends AbstractConfiguration
//add a decorator that will find introspectable annotations
context.getObjectFactory().addDecorator(new AnnotationDecorator(context)); //this must be the last Decorator because they are run in reverse order!
LOG.debug("configured {}", this);
if (LOG.isDebugEnabled())
LOG.debug("configured {}", this);
}
}
@ -207,57 +211,97 @@ public class QuickStartConfiguration extends AbstractConfiguration
protected void quickStart(WebAppContext context)
throws Exception
{
LOG.info("Quickstarting {}", context);
if (LOG.isDebugEnabled())
LOG.info("Quickstarting {}", context);
_quickStart = true;
context.setConfigurations(context.getConfigurations().stream()
.filter(c -> !__replacedConfigurations.contains(c.replaces()) && !__replacedConfigurations.contains(c.getClass())).toList().toArray(new Configuration[]{}));
context.getMetaData().setWebDescriptor(new WebDescriptor((Resource)context.getAttribute(QUICKSTART_WEB_XML)));
.filter(c -> !__replacedConfigurations.contains(c.replaces()))
.filter(c -> !__replacedConfigurations.contains(c.getClass()))
.toArray(Configuration[]::new));
Path quickStartWebXml = getQuickStartWebXml(context);
if (!Files.exists(quickStartWebXml))
throw new IllegalStateException("Quickstart doesn't exist: " + quickStartWebXml);
Resource quickStartWebResource = context.getResourceFactory().newResource(quickStartWebXml);
context.getMetaData().setWebDescriptor(new WebDescriptor(quickStartWebResource));
context.getContext().getServletContext().setEffectiveMajorVersion(context.getMetaData().getWebDescriptor().getMajorVersion());
context.getContext().getServletContext().setEffectiveMinorVersion(context.getMetaData().getWebDescriptor().getMinorVersion());
}
/**
* Get the quickstart-web.xml file as a Resource.
* Get the quickstart-web.xml Path from the webapp (from attributes if present, or built from the context's {@link WebAppContext#getWebInf()}).
*
* @param context the web app context
* @return the Resource for the quickstart-web.xml
* @throws Exception if unable to find the quickstart xml
* @return the Path for the quickstart-web.xml
* @throws IOException if unable to build the quickstart xml
*/
public Resource getQuickStartWebXml(WebAppContext context) throws Exception
public static Path getQuickStartWebXml(WebAppContext context) throws IOException
{
Object attr = context.getAttribute(QUICKSTART_WEB_XML);
if (attr instanceof Resource)
return (Resource)attr;
if (attr instanceof Path)
return (Path)attr;
Resource webInf = context.getWebInf();
if (webInf == null || !webInf.exists())
{
Files.createDirectories(context.getBaseResource().getPath().resolve("WEB-INF"));
webInf = context.getWebInf();
}
Path webInfDir = getWebInfPath(context);
Path qstartPath = webInfDir.resolve("quickstart-web.xml");
Resource qstart;
if (attr == null || StringUtil.isBlank(attr.toString()))
{
// TODO: should never return from WEB-INF/lib/foo.jar!/WEB-INF/quickstart-web.xml
// TODO: should also never return from a META-INF/versions/#/WEB-INF/quickstart-web.xml location
qstart = webInf.resolve("quickstart-web.xml");
}
else
if (attr != null && StringUtil.isNotBlank(attr.toString()))
{
Resource resource;
String attrValue = attr.toString();
try
{
// Try a relative resolution
qstart = _resourceFactory.newResource(webInf.getPath().resolve(attr.toString()));
resource = context.getResourceFactory().newResource(webInfDir.resolve(attrValue));
}
catch (Throwable th)
{
// try as a resource
qstart = _resourceFactory.newResource(attr.toString());
resource = context.getResourceFactory().newResource(attrValue);
}
if (resource != null)
{
Path attrPath = resource.getPath();
if (attrPath != null)
{
if (LOG.isDebugEnabled())
LOG.debug("Using quickstart attribute {} value of {}", attr, attrValue);
qstartPath = attrPath;
}
}
context.setAttribute(QUICKSTART_WEB_XML, qstart);
}
context.setAttribute(QUICKSTART_WEB_XML, qstart);
return qstart;
if (LOG.isDebugEnabled())
LOG.debug("Using quickstart location: {}", qstartPath);
context.setAttribute(QUICKSTART_WEB_XML, qstartPath);
return qstartPath;
}
private static Path getWebInfPath(WebAppContext context) throws IOException
{
Path webInfDir = null;
Resource webInf = context.getWebInf();
if (webInf != null)
{
webInfDir = webInf.getPath();
}
if (webInfDir == null)
{
Path baseResourcePath = findFirstWritablePath(context);
webInfDir = baseResourcePath.resolve("WEB-INF");
if (!Files.exists(webInfDir))
Files.createDirectories(webInfDir);
}
return webInfDir;
}
private static Path findFirstWritablePath(WebAppContext context) throws IOException
{
for (Resource resource: context.getBaseResource())
{
Path path = resource.getPath();
if (path == null || !Files.isDirectory(path) || !Files.isWritable(path))
continue; // skip
return path;
}
throw new IOException("Unable to find writable path in Base Resources");
}
}

View File

@ -17,6 +17,7 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@ -56,7 +57,6 @@ import org.eclipse.jetty.ee10.webapp.WebInfConfiguration;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.util.QuotedStringTokenizer;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.security.Constraint;
import org.eclipse.jetty.xml.XmlAppendable;
import org.slf4j.Logger;
@ -81,7 +81,7 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration
protected final boolean _abort;
protected String _originAttribute;
protected int _count;
protected Resource _quickStartWebXml;
protected Path _quickStartWebXml;
public QuickStartGeneratorConfiguration()
{
@ -114,12 +114,12 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration
return _originAttribute;
}
public Resource getQuickStartWebXml()
public Path getQuickStartWebXml()
{
return _quickStartWebXml;
}
public void setQuickStartWebXml(Resource quickStartWebXml)
public void setQuickStartWebXml(Path quickStartWebXml)
{
_quickStartWebXml = quickStartWebXml;
}
@ -812,7 +812,7 @@ public class QuickStartGeneratorConfiguration extends AbstractConfiguration
{
MetaData metadata = context.getMetaData();
metadata.resolve(context);
try (OutputStream os = Files.newOutputStream(_quickStartWebXml.getPath()))
try (OutputStream os = Files.newOutputStream(_quickStartWebXml))
{
generateQuickStartWebXml(context, os);
LOG.info("Generated {}", _quickStartWebXml);

View File

@ -28,6 +28,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
@ -921,6 +922,12 @@ public class DefaultServlet extends HttpServlet
{
_response.reset();
}
@Override
public CompletableFuture<Void> writeInterim(int status, HttpFields headers)
{
return null;
}
}
private class ServletResourceService extends ResourceService implements ResourceService.WelcomeFactory

View File

@ -28,6 +28,8 @@ import jakarta.servlet.http.HttpServletRequest;
import org.eclipse.jetty.ee10.servlet.ServletRequestState.Action;
import org.eclipse.jetty.http.BadMessageException;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.io.Connection;
@ -42,6 +44,7 @@ import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.URIUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -66,19 +69,12 @@ public class ServletChannel implements Runnable
private ServletContextRequest _request;
private long _oldIdleTimeout;
private Callback _callback;
private boolean _expects100Continue;
// TODO:
private final Listener _combinedListener = NOOP_LISTENER;
/**
* Bytes written after interception (eg after compression)
*/
// Bytes written after interception (e.g. after compression).
private long _written;
public ServletChannel()
{
}
public void setCallback(Callback callback)
{
if (_callback != null)
@ -94,10 +90,9 @@ public class ServletChannel implements Runnable
_state = new ServletRequestState(this); // TODO can this be recycled?
_endPoint = request.getConnectionMetaData().getConnection().getEndPoint();
_connector = request.getConnectionMetaData().getConnector();
// TODO: can we do this?
_configuration = request.getConnectionMetaData().getHttpConfiguration();
_expects100Continue = request.getHeaders().contains(HttpHeader.EXPECT, HttpHeaderValue.CONTINUE.asString());
request.getHttpInput().init();
if (LOG.isDebugEnabled())
@ -326,7 +321,23 @@ public class ServletChannel implements Runnable
*/
public void continue100(int available) throws IOException
{
throw new UnsupportedOperationException();
if (isExpecting100Continue())
{
_expects100Continue = false;
if (available == 0)
{
if (isCommitted())
throw new IOException("Committed before 100 Continue");
try
{
getResponse().writeInterim(HttpStatus.CONTINUE_100, HttpFields.EMPTY).get();
}
catch (Throwable x)
{
throw IO.rethrow(x);
}
}
}
}
public void recycle()
@ -712,7 +723,7 @@ public class ServletChannel implements Runnable
public boolean isExpecting100Continue()
{
return false;
return _expects100Continue;
}
public boolean isExpecting102Processing()

View File

@ -1079,7 +1079,7 @@ public class ServletContextHandler extends ContextHandler implements Graceful
Resource baseResource = getBaseResource();
if (baseResource != null && baseResource.isAlias())
LOG.warn("BaseResource {} is aliased to {} in {}. May not be supported in future releases.",
baseResource, baseResource.getAlias(), this);
baseResource, baseResource.getTargetURI(), this);
if (_logger == null)
_logger = LoggerFactory.getLogger(ContextHandler.class.getName() + getLogNameSuffix());

View File

@ -21,6 +21,7 @@ import java.util.EnumSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import jakarta.servlet.ServletContext;
@ -646,47 +647,22 @@ public class ServletContextResponse extends ContextResponse
@Override
public void sendError(int sc, String msg) throws IOException
{
// TODO
switch (sc)
{
case -1:
{
_servletChannel.abort(new IOException(msg));
break;
}
case HttpStatus.PROCESSING_102:
{
// TODO: should we check whether an Expect: 102 header is present?
if (!isCommitted())
{
try (Blocker.Callback blocker = Blocker.callback())
{
_response.setStatus(HttpStatus.PROCESSING_102);
_response.write(true, null, blocker);
blocker.block();
}
}
break;
}
case HttpStatus.EARLY_HINT_103:
case -1 -> _servletChannel.abort(new IOException(msg));
case HttpStatus.PROCESSING_102, HttpStatus.EARLY_HINT_103 ->
{
if (!isCommitted())
{
try (Blocker.Callback blocker = Blocker.callback())
{
_response.setStatus(HttpStatus.EARLY_HINT_103);
_response.write(true, null, blocker);
CompletableFuture<Void> completable = _response.writeInterim(sc, _response.getHeaders().asImmutable());
blocker.completeWith(completable);
blocker.block();
}
}
break;
}
default:
{
// This is just a state change
getState().sendError(sc, msg);
break;
}
default -> getState().sendError(sc, msg);
}
}

View File

@ -14,6 +14,7 @@
package org.eclipse.jetty.ee10.servlet.security.authentication;
import java.nio.ByteBuffer;
import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
import org.eclipse.jetty.ee10.servlet.security.Authentication;
@ -194,5 +195,11 @@ public class DeferredAuthentication implements Authentication.Deferred
public void reset()
{
}
@Override
public CompletableFuture<Void> writeInterim(int status, HttpFields headers)
{
return null;
}
};
}

View File

@ -17,19 +17,6 @@
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<org.eclipse.jetty.test.client.transport.H3.enable>${h3.test.enabled}</org.eclipse.jetty.test.client.transport.H3.enable>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>enable-incubator-foreign</id>

View File

@ -16,6 +16,7 @@ package org.eclipse.jetty.ee10.test.client.transport;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.EnumSet;
import java.util.List;
@ -69,25 +70,19 @@ public class AbstractTest
protected HttpClient client;
protected Path unixDomainPath;
private static EnumSet<Transport> allTransports()
public static Collection<Transport> transports()
{
EnumSet<Transport> transports = EnumSet.allOf(Transport.class);
// Disable H3 tests unless explicitly enabled with a system property.
if (!Boolean.getBoolean("org.eclipse.jetty.test.client.transport.H3.enable"))
if ("ci".equals(System.getProperty("env")))
transports.remove(Transport.H3);
return transports;
}
public static List<Transport> transports()
public static Collection<Transport> transportsNoFCGI()
{
return List.copyOf(allTransports());
}
public static List<Transport> transportsNoFCGI()
{
EnumSet<Transport> transports = allTransports();
Collection<Transport> transports = transports();
transports.remove(Transport.FCGI);
return List.copyOf(transports);
return transports;
}
@AfterEach

View File

@ -36,19 +36,21 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.ContinueProtocolHandler;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.AsyncRequestContent;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.client.util.BytesRequestContent;
import org.eclipse.jetty.ee10.servlet.ServletContextRequest;
import org.eclipse.jetty.client.util.FutureResponseListener;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpTester;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.IO;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@ -59,21 +61,18 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
// TODO: re-enable this test when 100 continue is implemented.
@Disabled
public class HttpClientContinueTest extends AbstractTest
{
@ParameterizedTest
@MethodSource("transports")
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithOneContentRespond100Continue(Transport transport) throws Exception
{
testExpect100ContinueRespond100Continue(transport, "data1".getBytes(StandardCharsets.UTF_8));
}
@ParameterizedTest
@MethodSource("transports")
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithMultipleContentsRespond100Continue(Transport transport) throws Exception
{
testExpect100ContinueRespond100Continue(transport, "data1".getBytes(StandardCharsets.UTF_8), "data2".getBytes(StandardCharsets.UTF_8), "data3".getBytes(StandardCharsets.UTF_8));
@ -112,7 +111,7 @@ public class HttpClientContinueTest extends AbstractTest
}
@ParameterizedTest
@MethodSource("transports")
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithChunkedContentRespond100Continue(Transport transport) throws Exception
{
start(transport, new HttpServlet()
@ -159,14 +158,14 @@ public class HttpClientContinueTest extends AbstractTest
}
@ParameterizedTest
@MethodSource("transports")
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithContentRespond417ExpectationFailed(Transport transport) throws Exception
{
testExpect100ContinueWithContentRespondError(transport, 417);
}
@ParameterizedTest
@MethodSource("transports")
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithContentRespond413RequestEntityTooLarge(Transport transport) throws Exception
{
testExpect100ContinueWithContentRespondError(transport, 413);
@ -209,7 +208,7 @@ public class HttpClientContinueTest extends AbstractTest
}
@ParameterizedTest
@MethodSource("transports")
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithContentWithRedirect(Transport transport) throws Exception
{
String data = "success";
@ -255,7 +254,7 @@ public class HttpClientContinueTest extends AbstractTest
}
@ParameterizedTest
@MethodSource("transports")
@MethodSource("transportsNoFCGI")
public void testRedirectWithExpect100ContinueWithContent(Transport transport) throws Exception
{
// A request with Expect: 100-Continue cannot receive non-final responses like 3xx
@ -304,7 +303,7 @@ public class HttpClientContinueTest extends AbstractTest
}
@ParameterizedTest
@MethodSource("transports")
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithContentWithResponseFailureBefore100Continue(Transport transport) throws Exception
{
AtomicReference<org.eclipse.jetty.client.api.Request> clientRequestRef = new AtomicReference<>();
@ -353,7 +352,7 @@ public class HttpClientContinueTest extends AbstractTest
}
@ParameterizedTest
@MethodSource("transports")
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithContentWithResponseFailureAfter100Continue(Transport transport) throws Exception
{
AtomicReference<org.eclipse.jetty.client.api.Request> clientRequestRef = new AtomicReference<>();
@ -403,7 +402,7 @@ public class HttpClientContinueTest extends AbstractTest
}
@ParameterizedTest
@MethodSource("transports")
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithContentWithResponseFailureDuring100Continue(Transport transport) throws Exception
{
start(transport, new HttpServlet()
@ -461,7 +460,7 @@ public class HttpClientContinueTest extends AbstractTest
}
@ParameterizedTest
@MethodSource("transports")
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithDeferredContentRespond100Continue(Transport transport) throws Exception
{
byte[] chunk1 = new byte[]{0, 1, 2, 3};
@ -531,7 +530,7 @@ public class HttpClientContinueTest extends AbstractTest
}
@ParameterizedTest
@MethodSource("transports")
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithInitialAndDeferredContentRespond100Continue(Transport transport) throws Exception
{
AtomicReference<Thread> handlerThread = new AtomicReference<>();
@ -581,7 +580,7 @@ public class HttpClientContinueTest extends AbstractTest
}
@ParameterizedTest
@MethodSource("transports")
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithConcurrentDeferredContentRespond100Continue(Transport transport) throws Exception
{
start(transport, new HttpServlet()
@ -620,7 +619,7 @@ public class HttpClientContinueTest extends AbstractTest
}
@ParameterizedTest
@MethodSource("transports")
@MethodSource("transportsNoFCGI")
public void testExpect100ContinueWithInitialAndConcurrentDeferredContentRespond100Continue(Transport transport) throws Exception
{
start(transport, new HttpServlet()
@ -676,17 +675,14 @@ public class HttpClientContinueTest extends AbstractTest
assertTrue(latch.await(5, TimeUnit.SECONDS));
}
@ParameterizedTest
@MethodSource("transports")
public void testExpect100ContinueWithTwoResponsesInOneRead(Transport transport) throws Exception
@Test
public void testExpect100ContinueWithTwoResponsesInOneRead() throws Exception
{
assumeTrue(transport == Transport.HTTP || transport == Transport.HTTPS);
// There is a chance that the server replies with the 100 Continue response
// and immediately after with the "normal" response, say a 200 OK.
// These may be read by the client in a single read, and must be handled correctly.
startClient(transport);
startClient(Transport.HTTP);
try (ServerSocket server = new ServerSocket())
{
@ -705,7 +701,7 @@ public class HttpClientContinueTest extends AbstractTest
try (Socket socket = server.accept())
{
// Read the request headers.
// Read only the request headers.
readRequestHeaders(socket.getInputStream());
OutputStream output = socket.getOutputStream();
@ -737,49 +733,62 @@ public class HttpClientContinueTest extends AbstractTest
}
}
@ParameterizedTest
@MethodSource("transports")
// TODO: figure out a way to force the 100 continue response.
@Disabled
public void testNoExpectRespond100Continue(Transport transport) throws Exception
@Test
public void testNoExpectRespond100Continue() throws Exception
{
start(transport, new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
ServletContextRequest jettyRequest = (ServletContextRequest)request;
// Force a 100 Continue response.
// TODO: figure out a way to force the 100 continue response.
// jettyRequest.getHttpChannel().sendResponse(HttpGenerator.CONTINUE_100_INFO, null, false);
// Echo the content.
IO.copy(request.getInputStream(), response.getOutputStream());
}
});
byte[] bytes = new byte[1024];
new Random().nextBytes(bytes);
ContentResponse response = client.newRequest(newURI(transport))
.body(new BytesRequestContent(bytes))
.timeout(5, TimeUnit.SECONDS)
.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
assertArrayEquals(bytes, response.getContent());
}
@ParameterizedTest
@MethodSource("transports")
public void testNoExpect100ContinueThenRedirectThen100ContinueThenResponse(Transport transport) throws Exception
{
assumeTrue(transport == Transport.HTTP || transport == Transport.HTTPS);
startClient(transport);
startClient(Transport.HTTP);
client.setMaxConnectionsPerDestination(1);
try (ServerSocket server = new ServerSocket())
{
server.bind(new InetSocketAddress("localhost", 0));
System.err.println("server listening on localhost:" + server.getLocalPort());
byte[] bytes = new byte[1024];
new Random().nextBytes(bytes);
Request clientRequest = client.newRequest("localhost", server.getLocalPort())
.body(new BytesRequestContent(bytes))
.timeout(5, TimeUnit.SECONDS);
FutureResponseListener listener = new FutureResponseListener(clientRequest);
clientRequest.send(listener);
try (Socket socket = server.accept())
{
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream();
HttpTester.Request serverRequest = HttpTester.parseRequest(input);
assertNotNull(serverRequest);
byte[] content = serverRequest.getContentBytes();
String serverResponse = """
HTTP/1.1 100 Continue\r
\r
HTTP/1.1 200 OK\r
Content-Length: $L\r
\r
""".replace("$L", String.valueOf(content.length));
output.write(serverResponse.getBytes(StandardCharsets.UTF_8));
output.write(content);
output.flush();
}
ContentResponse response = listener.get(5, TimeUnit.SECONDS);
assertEquals(HttpStatus.OK_200, response.getStatus());
assertArrayEquals(bytes, response.getContent());
}
}
@Test
public void testNoExpect100ContinueThen100ContinueThenRedirectThen100ContinueThenResponse() throws Exception
{
startClient(Transport.HTTP);
client.setMaxConnectionsPerDestination(1);
try (ServerSocket server = new ServerSocket())
{
server.bind(new InetSocketAddress("localhost", 0));
System.err.println("server listening on localhost:" + server.getLocalPort());
// No Expect header, no content.
CountDownLatch latch = new CountDownLatch(1);
@ -795,7 +804,7 @@ public class HttpClientContinueTest extends AbstractTest
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream();
readRequestHeaders(input);
HttpTester.parseRequest(input);
String response1 = """
HTTP/1.1 100 Continue\r
\r
@ -807,7 +816,7 @@ public class HttpClientContinueTest extends AbstractTest
output.write(response1.getBytes(StandardCharsets.UTF_8));
output.flush();
readRequestHeaders(input);
HttpTester.parseRequest(input);
String response2 = """
HTTP/1.1 100 Continue\r
\r

View File

@ -14,8 +14,6 @@
package org.eclipse.jetty.ee10.test.client.transport;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
@ -23,17 +21,10 @@ import java.util.concurrent.atomic.AtomicReference;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.eclipse.jetty.client.HttpConversation;
import org.eclipse.jetty.client.HttpExchange;
import org.eclipse.jetty.client.HttpRequest;
import org.eclipse.jetty.client.ProtocolHandler;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.BufferingResponseListener;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpStatus;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@ -42,9 +33,6 @@ import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertTrue;
// TODO: these tests fail with "last already written" since in core Jetty
// we don't handle well writing 2 HTTP responses one after the other.
@Disabled
public class InformationalResponseTest extends AbstractTest
{
@ParameterizedTest
@ -65,50 +53,6 @@ public class InformationalResponseTest extends AbstractTest
long idleTimeout = 10000;
setStreamIdleTimeout(idleTimeout);
CountDownLatch processingLatch = new CountDownLatch(1);
client.getProtocolHandlers().put(new ProtocolHandler()
{
@Override
public String getName()
{
return "Processing";
}
@Override
public boolean accept(org.eclipse.jetty.client.api.Request request, Response response)
{
return response.getStatus() == HttpStatus.PROCESSING_102;
}
@Override
public Response.Listener getResponseListener()
{
return new Response.Listener()
{
@Override
public void onSuccess(Response response)
{
processingLatch.countDown();
var request = response.getRequest();
HttpConversation conversation = ((HttpRequest)request).getConversation();
// Reset the conversation listeners, since we are going to receive another response code
conversation.updateResponseListeners(null);
HttpExchange exchange = conversation.getExchanges().peekLast();
if (exchange != null && response.getStatus() == HttpStatus.PROCESSING_102)
{
// All good, continue.
exchange.resetResponse();
}
else
{
response.abort(new IllegalStateException("should not have accepted"));
}
}
};
}
});
CountDownLatch completeLatch = new CountDownLatch(1);
AtomicReference<Response> response = new AtomicReference<>();
BufferingResponseListener listener = new BufferingResponseListener()
@ -122,11 +66,9 @@ public class InformationalResponseTest extends AbstractTest
};
client.newRequest(newURI(transport))
.method("GET")
.headers(headers -> headers.put(HttpHeader.EXPECT, HttpHeaderValue.PROCESSING))
.timeout(10, TimeUnit.SECONDS)
.send(listener);
assertTrue(processingLatch.await(10, TimeUnit.SECONDS));
assertTrue(completeLatch.await(10, TimeUnit.SECONDS));
assertThat(response.get().getStatus(), is(200));
assertThat(listener.getContentAsString(), is("OK"));
@ -141,11 +83,11 @@ public class InformationalResponseTest extends AbstractTest
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException
{
response.setHeader("Hint", "one");
response.addHeader("Hint", "one");
response.sendError(HttpStatus.EARLY_HINT_103);
response.setHeader("Hint", "two");
response.addHeader("Hint", "two");
response.sendError(HttpStatus.EARLY_HINT_103);
response.setHeader("Hint", "three");
response.addHeader("Hint", "three");
response.setStatus(200);
response.getOutputStream().print("OK");
}
@ -153,50 +95,6 @@ public class InformationalResponseTest extends AbstractTest
long idleTimeout = 10000;
setStreamIdleTimeout(idleTimeout);
List<String> hints = new CopyOnWriteArrayList<>();
client.getProtocolHandlers().put(new ProtocolHandler()
{
@Override
public String getName()
{
return "EarlyHint";
}
@Override
public boolean accept(org.eclipse.jetty.client.api.Request request, Response response)
{
return response.getStatus() == HttpStatus.EARLY_HINT_103;
}
@Override
public Response.Listener getResponseListener()
{
return new Response.Listener()
{
@Override
public void onSuccess(Response response)
{
var request = response.getRequest();
HttpConversation conversation = ((HttpRequest)request).getConversation();
// Reset the conversation listeners, since we are going to receive another response code
conversation.updateResponseListeners(null);
HttpExchange exchange = conversation.getExchanges().peekLast();
if (exchange != null && response.getStatus() == HttpStatus.EARLY_HINT_103)
{
// All good, continue.
hints.add(response.getHeaders().get("Hint"));
exchange.resetResponse();
}
else
{
response.abort(new IllegalStateException("should not have accepted"));
}
}
};
}
});
CountDownLatch complete = new CountDownLatch(1);
AtomicReference<Response> response = new AtomicReference<>();
BufferingResponseListener listener = new BufferingResponseListener()
@ -204,7 +102,6 @@ public class InformationalResponseTest extends AbstractTest
@Override
public void onComplete(Result result)
{
hints.add(result.getResponse().getHeaders().get("Hint"));
response.set(result.getResponse());
complete.countDown();
}
@ -217,6 +114,6 @@ public class InformationalResponseTest extends AbstractTest
assertTrue(complete.await(5, TimeUnit.SECONDS));
assertThat(response.get().getStatus(), is(200));
assertThat(listener.getContentAsString(), is("OK"));
assertThat(hints, contains("one", "two", "three"));
assertThat(response.get().getHeaders().getValuesList("Hint"), contains("one", "two", "three"));
}
}

View File

@ -19,7 +19,6 @@ import org.eclipse.jetty.ee10.plus.webapp.PlusConfiguration;
import org.eclipse.jetty.ee10.webapp.WebAppContext;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.eclipse.jetty.xml.XmlConfiguration;
public class Quickstart
@ -37,7 +36,7 @@ public class Quickstart
WebAppContext webapp = new WebAppContext();
Resource contextXml = null;
if (args.length > 1)
contextXml = ResourceFactory.of(webapp).newResource(args[1]);
contextXml = webapp.getResourceFactory().newResource(args[1]);
Server server = new Server(8080);

View File

@ -1253,7 +1253,7 @@ public class WebAppContext extends ServletContextHandler implements WebAppClassL
public void setExtraClasspath(String extraClasspath)
{
List<URI> uris = URIUtil.split(extraClasspath);
setExtraClasspath(ResourceFactory.of(this).newResource(uris));
setExtraClasspath(this.getResourceFactory().newResource(uris));
}
public void setExtraClasspath(ResourceCollection extraClasspath)

View File

@ -41,7 +41,6 @@ public class WebInfConfiguration extends AbstractConfiguration
public static final String TEMPORARY_RESOURCE_BASE = "org.eclipse.jetty.webapp.tmpResourceBase";
protected Resource _preUnpackBaseResource;
private ResourceFactory.Closeable _resourceFactory;
public WebInfConfiguration()
{
@ -93,9 +92,6 @@ public class WebInfConfiguration extends AbstractConfiguration
if (tmpdirConfigured != null && !tmpdirConfigured)
context.setTempDirectory(null);
IO.close(_resourceFactory);
_resourceFactory = null;
//reset the base resource back to what it was before we did any unpacking of resources
//TODO there is something wrong with the config of the resource base as this should never be null
context.setBaseResource(_preUnpackBaseResource == null ? null : _preUnpackBaseResource);
@ -282,7 +278,6 @@ public class WebInfConfiguration extends AbstractConfiguration
public void unpack(WebAppContext context) throws IOException
{
Resource webApp = context.getBaseResource();
_resourceFactory = ResourceFactory.closeable();
_preUnpackBaseResource = context.getBaseResource();
if (webApp == null)
@ -296,12 +291,13 @@ public class WebInfConfiguration extends AbstractConfiguration
if (webApp == null)
throw new IllegalStateException("No resourceBase or war set for context");
// Accept aliases for WAR files
if (webApp.isAlias())
// Use real location (if different) for WAR file, so that change/modification monitoring can work.
URI targetURI = webApp.getTargetURI();
if (targetURI != null)
{
if (LOG.isDebugEnabled())
LOG.debug("{} anti-aliased to {}", webApp, webApp.getAlias());
webApp = context.newResource(webApp.getAlias());
LOG.debug("{} anti-aliased to {}", webApp, targetURI);
webApp = context.newResource(targetURI);
}
if (LOG.isDebugEnabled())
@ -317,7 +313,7 @@ public class WebInfConfiguration extends AbstractConfiguration
if (webApp.exists() && !webApp.isDirectory() && !webApp.toString().startsWith("jar:"))
{
// No - then lets see if it can be turned into a jar URL.
webApp = _resourceFactory.newJarFileResource(webApp.getURI());
webApp = context.getResourceFactory().newJarFileResource(webApp.getURI());
}
// If we should extract or the URL is still not usable
@ -334,7 +330,7 @@ public class WebInfConfiguration extends AbstractConfiguration
if (war != null)
{
// look for a sibling like "foo/" to a "foo.war"
Path warfile = _resourceFactory.newResource(war).getPath();
Path warfile = context.getResourceFactory().newResource(war).getPath();
if (warfile != null && warfile.getFileName().toString().toLowerCase(Locale.ENGLISH).endsWith(".war"))
{
Path sibling = warfile.getParent().resolve(warfile.getFileName().toString().substring(0, warfile.getFileName().toString().length() - 4));
@ -397,7 +393,7 @@ public class WebInfConfiguration extends AbstractConfiguration
}
}
}
webApp = _resourceFactory.newResource(extractedWebAppDir.normalize());
webApp = context.getResourceFactory().newResource(extractedWebAppDir.normalize());
}
// Now do we have something usable?
@ -450,7 +446,7 @@ public class WebInfConfiguration extends AbstractConfiguration
webInfClasses.copyTo(webInfClassesDir.toPath());
}
webInf = _resourceFactory.newResource(extractedWebInfDir.getCanonicalPath());
webInf = context.getResourceFactory().newResource(extractedWebInfDir.getCanonicalPath());
Resource rc = Resource.combine(webInf, webApp);

View File

@ -49,7 +49,7 @@ public class WebXmlConfiguration extends AbstractConfiguration
String defaultsDescriptor = context.getDefaultsDescriptor();
if (defaultsDescriptor != null && defaultsDescriptor.length() > 0)
{
Resource dftResource = ResourceFactory.of(context).newSystemResource(defaultsDescriptor);
Resource dftResource = context.getResourceFactory().newSystemResource(defaultsDescriptor);
if (dftResource == null)
{
String pkg = WebXmlConfiguration.class.getPackageName().replace(".", "/") + "/";
@ -83,7 +83,7 @@ public class WebXmlConfiguration extends AbstractConfiguration
{
if (overrideDescriptor != null && overrideDescriptor.length() > 0)
{
Resource orideResource = ResourceFactory.of(context).newSystemResource(overrideDescriptor);
Resource orideResource = context.getResourceFactory().newSystemResource(overrideDescriptor);
if (orideResource == null)
orideResource = context.newResource(overrideDescriptor);
context.getMetaData().addOverrideDescriptor(new OverrideDescriptor(orideResource));

View File

@ -30,7 +30,6 @@ import org.eclipse.jetty.util.IO;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.resource.FileSystemPool;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -63,7 +62,7 @@ public class WebAppClassLoaderTest
_testWebappDir = MavenTestingUtils.getProjectDirPath("src/test/webapp");
_context = new WebAppContext();
Resource webapp = ResourceFactory.of(_context).newResource(_testWebappDir);
Resource webapp = _context.getResourceFactory().newResource(_testWebappDir);
_context.setBaseResource(webapp);
_context.setContextPath("/test");
_context.setExtraClasspath("src/test/resources/ext/*");

View File

@ -44,7 +44,6 @@ import org.eclipse.jetty.toolchain.test.JAR;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.component.ContainerLifeCycle;
import org.eclipse.jetty.util.resource.ResourceFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -154,7 +153,7 @@ public class WebAppTester extends ContainerLifeCycle
// Configure the WebAppContext.
_context = new WebAppContext();
_context.setContextPath(contextPath);
_context.setBaseResource(ResourceFactory.of(_context).newResource(_contextDir).getPath());
_context.setBaseResource(_context.getResourceFactory().newResource(_contextDir).getPath());
_context.setConfigurations(new Configuration[]
{

View File

@ -128,13 +128,6 @@
<groupId>org.infinispan</groupId>
<artifactId>infinispan-client-hotrod</artifactId>
<scope>test</scope>
<exclusions>
<!-- this is depending on an old log4j version which have this issue https://issues.apache.org/jira/browse/LOG4J2-3241 -->
<exclusion>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.infinispan</groupId>

Some files were not shown because too many files have changed in this diff Show More