diff --git a/changes.md b/changes.md
index ba3e8a0b2b..dac4140c09 100644
--- a/changes.md
+++ b/changes.md
@@ -43,6 +43,13 @@ Changes log
the connector for synchronous calls, without leaking credentials to other origins.
- Removed `JettyServerHelper`, `JettyServerCall`, `JettyClientCall`, `JettyHandler`,
`RestletSslContextFactoryClient` and `RestletSslContextFactoryServer`.
+ - Removed the `org.restlet.engine.adapter` package (`Adapter`, `Call`, `ClientAdapter`,
+ `ClientCall`, `HttpClientHelper`, `HttpRequest`, `HttpResponse`, `HttpServerHelper`,
+ `ServerAdapter`, `ServerCall`) and the "adapter" connector parameter. The Netty
+ connectors now map Netty messages directly to Restlet requests and responses, and
+ `NettyServerHelper` and `NettyClientHelper` extend `ServerHelper` and `ClientHelper`.
+ Custom HTTP connectors should extend those classes. Request headers remain available
+ with `Message.getHeaders()`.
- `MultiPartRepresentation` no longer exposes Jetty types: parts are instances of the new
`MultiPartRepresentation.Part` class (name, file name, headers, content
representation), parsing takes an optional storage directory instead of a Jetty
@@ -54,6 +61,11 @@ Changes log
- Removed the `Protocol.WAR` constant ("war" scheme, Web Archive Access Protocol), a
leftover of the Servlet mode, which is no longer supported. `Protocol.valueOf("war")`
now returns a new, unknown protocol. Use CLAP or FILE references instead.
+ - Enhancements
+ - Added `Response.getProtocol()` and `setProtocol()`: the protocol, with its version,
+ used by the server to reply (for example HTTP/1.1, HTTP/2.0 or HTTP/3.0), set by the HTTP
+ client connector. When unknown, the protocol of the request is returned.
+ `Response.toString()` now shows it.
- Bugs fixed
- Slf4j bridge reports wrong line number and caller class in log4j. Issue #233.
- When GET is an allowed method of a server resource, HEAD is now also listed (e.g. in the Allow header of a 405 response). Issue #266.
diff --git a/org.restlet/src/main/java/module-info.java b/org.restlet/src/main/java/module-info.java
index 7d442b7859..520bad7227 100644
--- a/org.restlet/src/main/java/module-info.java
+++ b/org.restlet/src/main/java/module-info.java
@@ -33,7 +33,6 @@
exports org.restlet;
exports org.restlet.data;
exports org.restlet.engine;
- exports org.restlet.engine.adapter;
exports org.restlet.engine.application;
exports org.restlet.engine.component;
exports org.restlet.engine.connector;
diff --git a/org.restlet/src/main/java/org/restlet/Response.java b/org.restlet/src/main/java/org/restlet/Response.java
index 3790d3b65d..aff2c76c79 100644
--- a/org.restlet/src/main/java/org/restlet/Response.java
+++ b/org.restlet/src/main/java/org/restlet/Response.java
@@ -19,6 +19,7 @@
import org.restlet.data.CookieSetting;
import org.restlet.data.Dimension;
import org.restlet.data.Method;
+import org.restlet.data.Protocol;
import org.restlet.data.Reference;
import org.restlet.data.ServerInfo;
import org.restlet.data.Status;
@@ -135,6 +136,9 @@ public static void setCurrent(Response response) {
/** Indicates how long the service is expected to be unavailable to the requesting client. */
private volatile Date retryAfter;
+ /** The protocol, with its version, used by the server to reply. */
+ private volatile Protocol protocol;
+
/** The server-specific information. */
private volatile ServerInfo serverInfo;
@@ -162,6 +166,7 @@ public Response(Request request) {
this.locationRef = null;
this.proxyChallengeRequests = null;
this.request = request;
+ this.protocol = null;
this.retryAfter = null;
this.serverInfo = null;
this.status = Status.SUCCESS_OK;
@@ -476,6 +481,18 @@ public Date getRetryAfter() {
return retryAfter;
}
+ /**
+ * Returns the protocol, with its version, used by the server to reply, if known. For example,
+ * the HTTP client connector sets "HTTP/1.1", "HTTP/2.0" or "HTTP/3.0" (secure exchanges use the
+ * HTTPS protocol). When unknown, returns the protocol of the request.
+ *
+ * @return The protocol used by the server to reply.
+ */
+ public Protocol getProtocol() {
+ Protocol result = this.protocol;
+ return (result == null && getRequest() != null) ? getRequest().getProtocol() : result;
+ }
+
/**
* Returns the server-specific information. Creates a new instance if no one has been set.
*
@@ -932,6 +949,15 @@ public void setRetryAfter(Date retryAfter) {
this.retryAfter = retryAfter;
}
+ /**
+ * Sets the protocol, with its version, used by the server to reply.
+ *
+ * @param protocol The protocol used by the server to reply.
+ */
+ public void setProtocol(Protocol protocol) {
+ this.protocol = protocol;
+ }
+
/**
* Sets the server-specific information.
*
@@ -987,6 +1013,7 @@ public void setStatus(Status status, Throwable throwable, String message) {
* @return A synthesis of the response like an HTTP status line.
*/
public String toString() {
- return ((getRequest() == null) ? "?" : getRequest().getProtocol()) + " - " + getStatus();
+ Protocol p = getProtocol();
+ return ((p == null) ? "?" : p) + " - " + getStatus();
}
}
diff --git a/org.restlet/src/main/java/org/restlet/engine/adapter/Adapter.java b/org.restlet/src/main/java/org/restlet/engine/adapter/Adapter.java
deleted file mode 100644
index 38bbabd770..0000000000
--- a/org.restlet/src/main/java/org/restlet/engine/adapter/Adapter.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Copyright 2005-2026 Qlik
- *
- * The content of this file is subject to the terms of the Apache 2.0 open
- * source license available at https://www.opensource.org/licenses/apache-2.0
- *
- * Restlet is a registered trademark of QlikTech International AB.
- */
-package org.restlet.engine.adapter;
-
-import java.util.logging.Logger;
-import org.restlet.Context;
-
-/**
- * Converter between high-level and low-level HTTP calls.
- *
- * @author Jerome Louvel
- */
-public class Adapter {
-
- /** The context. */
- private volatile Context context;
-
- /**
- * Constructor.
- *
- * @param context The context to use.
- */
- public Adapter(Context context) {
- this.context = context;
- }
-
- /**
- * Returns the context.
- *
- * @return The context.
- */
- public Context getContext() {
- return this.context;
- }
-
- /**
- * Returns the logger.
- *
- * @return The logger.
- */
- public Logger getLogger() {
- Logger result = (getContext() != null) ? getContext().getLogger() : null;
- return (result != null) ? result : Context.getCurrentLogger();
- }
-}
diff --git a/org.restlet/src/main/java/org/restlet/engine/adapter/Call.java b/org.restlet/src/main/java/org/restlet/engine/adapter/Call.java
deleted file mode 100644
index 73326f7e8a..0000000000
--- a/org.restlet/src/main/java/org/restlet/engine/adapter/Call.java
+++ /dev/null
@@ -1,481 +0,0 @@
-/**
- * Copyright 2005-2026 Qlik
- *
- * The content of this file is subject to the terms of the Apache 2.0 open
- * source license available at https://www.opensource.org/licenses/apache-2.0
- *
- * Restlet is a registered trademark of QlikTech International AB.
- */
-package org.restlet.engine.adapter;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.logging.Logger;
-import org.restlet.Context;
-import org.restlet.data.Header;
-import org.restlet.data.Protocol;
-import org.restlet.engine.header.HeaderUtils;
-import org.restlet.representation.InputRepresentation;
-import org.restlet.representation.Representation;
-import org.restlet.util.Series;
-
-/**
- * Low-level call for the HTTP connectors.
- *
- * @author Jerome Louvel
- */
-public abstract class Call {
- /**
- * Returns true if the given exception is caused by a broken connection.
- *
- * @param exception The exception to inspect.
- * @return True if the given exception is caused by a broken connection.
- */
- public static boolean isBroken(Throwable exception) {
- boolean result = false;
-
- // detect client abort exceptions
- if (exception instanceof IOException) {
- String exceptionName = exception.getClass().getName();
- result = exceptionName.endsWith("ClientAbortException");
- }
-
- // check for known exception messages
- if (!result) {
- String exceptionMessage = exception.getMessage();
- if (exceptionMessage != null) {
- result =
- (exceptionMessage.contains("Broken pipe"))
- || (exceptionMessage.equals(
- "An existing connection must have been closed by the remote party.")
- || (exceptionMessage.equals(
- "An open connection has been abandonned by your network stack.")));
- }
- }
-
- if (!result && exception.getCause() != null) {
- result = isBroken(exception.getCause());
- }
-
- return result;
- }
-
- /** The client IP address. */
- private volatile String clientAddress;
-
- /** The client port. */
- private volatile int clientPort;
-
- /** Indicates if the call is confidential. */
- private volatile boolean confidential;
-
- /** The hostRef domain. */
- private volatile String hostDomain;
-
- /** The hostRef port. */
- private volatile int hostPort;
-
- /** The method. */
- private volatile String method;
-
- /** The exact protocol. */
- private volatile Protocol protocol;
-
- /** The reason phrase. */
- private volatile String reasonPhrase;
-
- /** The request headers. */
- private final Series requestHeaders;
-
- /** The request URI. */
- private volatile String requestUri;
-
- /** The response headers. */
- private final Series responseHeaders;
-
- /** The server IP address. */
- private volatile String serverAddress;
-
- /** The server port. */
- private volatile int serverPort;
-
- /** The status code. */
- private volatile int statusCode;
-
- /** The user principal. */
- private volatile java.security.Principal userPrincipal;
-
- /** The protocol version. */
- private volatile String version;
-
- /** Constructor. */
- protected Call() {
- this.hostDomain = null;
- this.hostPort = -1;
- this.clientAddress = null;
- this.clientPort = -1;
- this.confidential = false;
- this.method = null;
- this.protocol = null;
- this.reasonPhrase = "";
- this.requestHeaders = new Series<>(Header.class);
- this.requestUri = null;
- this.responseHeaders = new Series<>(Header.class);
- this.serverAddress = null;
- this.serverPort = -1;
- this.statusCode = 200;
- this.userPrincipal = null;
- this.version = null;
- }
-
- /**
- * Returns the client address.
- * Corresponds to the IP address of the requesting client.
- *
- * @return The client address.
- */
- public String getClientAddress() {
- return this.clientAddress;
- }
-
- /**
- * Returns the client port.
- * Corresponds to the TCP/IP port of the requesting client.
- *
- * @return The client port.
- */
- public int getClientPort() {
- return this.clientPort;
- }
-
- /**
- * Returns the host domain.
- *
- * @return The host domain.
- */
- public String getHostDomain() {
- return this.hostDomain;
- }
-
- /**
- * Returns the host port.
- *
- * @return The host port.
- */
- public int getHostPort() {
- return this.hostPort;
- }
-
- /**
- * Returns the logger.
- *
- * @return The logger.
- */
- public Logger getLogger() {
- return Context.getCurrentLogger();
- }
-
- /**
- * Returns the request method.
- *
- * @return The request method.
- */
- public String getMethod() {
- return this.method;
- }
-
- /**
- * Returns the exact protocol (HTTP or HTTPS).
- *
- * @return The exact protocol (HTTP or HTTPS).
- */
- public Protocol getProtocol() {
- if (this.protocol == null) {
- this.protocol = isConfidential() ? Protocol.HTTPS : Protocol.HTTP;
- }
- return this.protocol;
- }
-
- /**
- * Returns the reason phrase.
- *
- * @return The reason phrase.
- */
- public String getReasonPhrase() {
- return this.reasonPhrase;
- }
-
- /**
- * Returns the representation wrapping the given stream.
- *
- * @param stream The response input stream.
- * @return The wrapping representation.
- */
- protected Representation getRepresentation(InputStream stream) {
- return new InputRepresentation(stream, null);
- }
-
- /**
- * Returns the modifiable list of request headers.
- *
- * @return The modifiable list of request headers.
- */
- public Series getRequestHeaders() {
- return this.requestHeaders;
- }
-
- /**
- * Returns the URI on the request line (most like a relative reference, but not necessarily).
- *
- * @return The URI on the request line.
- */
- public String getRequestUri() {
- return this.requestUri;
- }
-
- /**
- * Returns the modifiable list of server headers.
- *
- * @return The modifiable list of server headers.
- */
- public Series getResponseHeaders() {
- return this.responseHeaders;
- }
-
- /**
- * Returns the response address.
- * Corresponds to the IP address of the responding server.
- *
- * @return The response address.
- */
- public String getServerAddress() {
- return this.serverAddress;
- }
-
- /**
- * Returns the server port.
- *
- * @return The server port.
- */
- public int getServerPort() {
- return this.serverPort;
- }
-
- /**
- * Returns the status code.
- *
- * @return The status code.
- */
- public int getStatusCode() {
- return this.statusCode;
- }
-
- /**
- * Returns the user principal.
- *
- * @return The user principal.
- */
- public java.security.Principal getUserPrincipal() {
- return this.userPrincipal;
- }
-
- /**
- * Returns the protocol version used.
- *
- * @return The protocol version used.
- */
- public String getVersion() {
- return this.version;
- }
-
- /**
- * Indicates if the client wants a persistent connection.
- *
- * @return True if the client wants a persistent connection.
- */
- protected abstract boolean isClientKeepAlive();
-
- /**
- * Indicates if the confidentiality of the call is ensured (ex: via SSL).
- *
- * @return True if the confidentiality of the call is ensured (ex: via SSL).
- */
- public boolean isConfidential() {
- return this.confidential;
- }
-
- /**
- * Returns true if the given exception is caused by a broken connection.
- *
- * @param exception The exception to inspect.
- * @return True if the given exception is caused by a broken connection.
- */
- public boolean isConnectionBroken(Throwable exception) {
- return isBroken(exception);
- }
-
- /**
- * Indicates if both the client and the server want a persistent connection.
- *
- * @return True if the connection should be kept alive after the call processing.
- */
- protected boolean isKeepAlive() {
- return isClientKeepAlive() && isServerKeepAlive();
- }
-
- /**
- * Indicates if the request entity is chunked.
- *
- * @return True if the request entity is chunked.
- */
- protected boolean isRequestChunked() {
- return HeaderUtils.isChunkedEncoding(getRequestHeaders());
- }
-
- /**
- * Indicates if the response entity is chunked.
- *
- * @return True if the response entity is chunked.
- */
- protected boolean isResponseChunked() {
- return HeaderUtils.isChunkedEncoding(getResponseHeaders());
- }
-
- /**
- * Indicates if the server wants a persistent connection.
- *
- * @return True if the server wants a persistent connection.
- */
- protected abstract boolean isServerKeepAlive();
-
- /**
- * Sets the client address.
- *
- * @param clientAddress The client address.
- */
- protected void setClientAddress(String clientAddress) {
- this.clientAddress = clientAddress;
- }
-
- /**
- * Sets the client port.
- *
- * @param clientPort The client port.
- */
- protected void setClientPort(int clientPort) {
- this.clientPort = clientPort;
- }
-
- /**
- * Indicates if the confidentiality of the call is ensured (ex: via SSL).
- *
- * @param confidential True if the confidentiality of the call is ensured (ex: via SSL).
- */
- protected void setConfidential(boolean confidential) {
- this.confidential = confidential;
- }
-
- /**
- * Sets the host domain name.
- *
- * @param hostDomain The baseRef domain name.
- */
- public void setHostDomain(String hostDomain) {
- this.hostDomain = hostDomain;
- }
-
- /**
- * Sets the host port.
- *
- * @param hostPort The host port.
- */
- public void setHostPort(int hostPort) {
- this.hostPort = hostPort;
- }
-
- /**
- * Sets the request method.
- *
- * @param method The request method.
- */
- protected void setMethod(String method) {
- this.method = method;
- }
-
- /**
- * Sets the exact protocol used (HTTP or HTTPS).
- *
- * @param protocol The protocol.
- */
- public void setProtocol(Protocol protocol) {
- this.protocol = protocol;
- }
-
- /**
- * Sets the reason phrase.
- *
- * @param reasonPhrase The reason phrase.
- */
- public void setReasonPhrase(String reasonPhrase) {
- this.reasonPhrase = reasonPhrase;
- }
-
- /**
- * Sets the full request URI.
- *
- * @param requestUri The full request URI.
- */
- protected void setRequestUri(String requestUri) {
- if ((requestUri == null) || (requestUri.isEmpty())) {
- requestUri = "/";
- }
-
- this.requestUri = requestUri;
- }
-
- /**
- * Sets the response address.
- * Corresponds to the IP address of the responding server.
- *
- * @param responseAddress The response address.
- */
- public void setServerAddress(String responseAddress) {
- this.serverAddress = responseAddress;
- }
-
- /**
- * Sets the server port.
- *
- * @param serverPort The server port.
- */
- public void setServerPort(int serverPort) {
- this.serverPort = serverPort;
- }
-
- /**
- * Sets the status code.
- *
- * @param code The status code.
- */
- public void setStatusCode(int code) {
- this.statusCode = code;
- }
-
- /**
- * Sets the user principal.
- *
- * @param principal The user principal.
- */
- public void setUserPrincipal(java.security.Principal principal) {
- this.userPrincipal = principal;
- }
-
- /**
- * Sets the protocol version used.
- *
- * @param version The protocol version used.
- */
- public void setVersion(String version) {
- this.version = version;
- }
-}
diff --git a/org.restlet/src/main/java/org/restlet/engine/adapter/ClientAdapter.java b/org.restlet/src/main/java/org/restlet/engine/adapter/ClientAdapter.java
deleted file mode 100644
index 72811ab245..0000000000
--- a/org.restlet/src/main/java/org/restlet/engine/adapter/ClientAdapter.java
+++ /dev/null
@@ -1,173 +0,0 @@
-/**
- * Copyright 2005-2026 Qlik
- *
- * The content of this file is subject to the terms of the Apache 2.0 open
- * source license available at https://www.opensource.org/licenses/apache-2.0
- *
- * Restlet is a registered trademark of QlikTech International AB.
- */
-package org.restlet.engine.adapter;
-
-import java.util.logging.Level;
-import org.restlet.Context;
-import org.restlet.Request;
-import org.restlet.Response;
-import org.restlet.Uniform;
-import org.restlet.data.Header;
-import org.restlet.data.Method;
-import org.restlet.data.Status;
-import org.restlet.engine.header.HeaderConstants;
-import org.restlet.engine.header.HeaderUtils;
-import org.restlet.util.Series;
-
-/**
- * Converter of high-level uniform calls into low-level HTTP client calls.
- *
- * @author Jerome Louvel
- */
-public class ClientAdapter extends Adapter {
-
- /**
- * Constructor.
- *
- * @param context The context to use.
- */
- public ClientAdapter(Context context) {
- super(context);
- }
-
- /**
- * Commits the changes to a handled HTTP client call back into the original uniform call. The
- * default implementation first invokes the "addResponseHeaders" then asks the "htppCall" to
- * send the response back to the client.
- *
- * @param httpCall The original HTTP call.
- * @param request The high-level request.
- * @param response The high-level response.
- */
- public void commit(final ClientCall httpCall, Request request, Response response) {
- if (httpCall != null) {
- // Check if the call is asynchronous
- if (request.isAsynchronous()) {
- final Uniform userCallback = request.getOnResponse();
-
- // Send the request to the client
- httpCall.sendRequest(
- request,
- response,
- (request1, response1) -> {
- try {
- updateResponse(
- response1,
- new Status(
- httpCall.getStatusCode(),
- httpCall.getReasonPhrase()),
- httpCall);
-
- if (userCallback != null) {
- userCallback.handle(request1, response1);
- }
- } catch (Exception exception) {
- getLogger()
- .log(
- Level.WARNING,
- "Unexpected error or exception inside the user call back",
- exception);
- }
- });
- } else {
- updateResponse(response, httpCall.sendRequest(request), httpCall);
- }
- }
- }
-
- /**
- * Reads the response headers of a handled HTTP client call to update the original uniform call.
- *
- * @param httpCall The handled HTTP client call.
- * @param response The high-level response to update.
- */
- protected void readResponseHeaders(ClientCall httpCall, Response response) {
- try {
- Series responseHeaders = httpCall.getResponseHeaders();
-
- // Put the response headers in the call's attributes map
- response.getAttributes().put(HeaderConstants.ATTRIBUTE_HEADERS, responseHeaders);
-
- HeaderUtils.copyResponseTransportHeaders(responseHeaders, response);
- } catch (Exception e) {
- getLogger()
- .log(
- Level.FINE,
- "An error occurred during the processing of the HTTP response.",
- e);
- response.setStatus(Status.CONNECTOR_ERROR_INTERNAL, e);
- }
- }
-
- /**
- * Converts a low-level HTTP call into a high-level uniform call.
- *
- * @param client The HTTP client that will handle the call.
- * @param request The high-level request.
- * @return A new high-level uniform call.
- */
- public ClientCall toSpecific(HttpClientHelper client, Request request) {
- // Create the low-level HTTP client call
- ClientCall result = client.create(request);
-
- // Add the headers
- if (result != null) {
- HeaderUtils.addGeneralHeaders(request, result.getRequestHeaders());
-
- if (request.getEntity() != null) {
- HeaderUtils.addEntityHeaders(request.getEntity(), result.getRequestHeaders());
- }
-
- // NOTE: This must stay at the end because the AWS challenge
- // scheme requires access to all HTTP headers
- HeaderUtils.addRequestHeaders(request, result.getRequestHeaders());
- }
-
- return result;
- }
-
- /**
- * Updates the response with information from the lower-level HTTP client call.
- *
- * @param response The response to modify.
- * @param status The response status to apply.
- * @param httpCall The source HTTP client call.
- */
- public void updateResponse(Response response, Status status, ClientCall httpCall) {
- // Send the request to the client
- response.setStatus(status);
-
- // Get the server address
- response.getServerInfo().setAddress(httpCall.getServerAddress());
- response.getServerInfo().setPort(httpCall.getServerPort());
-
- // Read the response headers
- readResponseHeaders(httpCall, response);
-
- // Set the entity
- response.setEntity(httpCall.getResponseEntity(response));
-
- // Release the representation's content for some obvious cases
- if (response.getEntity() != null) {
- if (response.getEntity().isEmpty()) {
- response.getEntity().release();
- } else if (response.getRequest().getMethod().equals(Method.HEAD)) {
- response.getEntity().release();
- } else if (response.getStatus().equals(Status.SUCCESS_NO_CONTENT)) {
- response.getEntity().release();
- } else if (response.getStatus().equals(Status.SUCCESS_RESET_CONTENT)
- || response.getStatus().isInformational()) {
- response.getEntity().release();
- response.setEntity(null);
- } else if (response.getStatus().equals(Status.REDIRECTION_NOT_MODIFIED)) {
- response.getEntity().release();
- }
- }
- }
-}
diff --git a/org.restlet/src/main/java/org/restlet/engine/adapter/ClientCall.java b/org.restlet/src/main/java/org/restlet/engine/adapter/ClientCall.java
deleted file mode 100644
index 8bce9a0bd0..0000000000
--- a/org.restlet/src/main/java/org/restlet/engine/adapter/ClientCall.java
+++ /dev/null
@@ -1,287 +0,0 @@
-/**
- * Copyright 2005-2026 Qlik
- *
- * The content of this file is subject to the terms of the Apache 2.0 open
- * source license available at https://www.opensource.org/licenses/apache-2.0
- *
- * Restlet is a registered trademark of QlikTech International AB.
- */
-package org.restlet.engine.adapter;
-
-import static org.restlet.data.Encoding.IDENTITY;
-import static org.restlet.data.Status.CONNECTOR_ERROR_COMMUNICATION;
-import static org.restlet.data.Status.REDIRECTION_NOT_MODIFIED;
-import static org.restlet.data.Status.SUCCESS_NO_CONTENT;
-import static org.restlet.data.Status.SUCCESS_RESET_CONTENT;
-import static org.restlet.representation.Representation.UNKNOWN_SIZE;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.logging.Level;
-import org.restlet.Context;
-import org.restlet.Request;
-import org.restlet.Response;
-import org.restlet.data.Header;
-import org.restlet.data.Method;
-import org.restlet.data.Status;
-import org.restlet.engine.connector.ConnectorHelper;
-import org.restlet.engine.header.HeaderConstants;
-import org.restlet.engine.header.HeaderUtils;
-import org.restlet.representation.Representation;
-import org.restlet.util.Series;
-
-/**
- * Low-level HTTP client call.
- *
- * @author Jerome Louvel
- */
-public abstract class ClientCall extends Call {
-
- /**
- * Returns the local IP address or 127.0.0.1 if the resolution fails.
- *
- * @return The local IP address or 127.0.0.1 if the resolution fails.
- */
- public static String getLocalAddress() {
- try {
- return java.net.InetAddress.getLocalHost().getHostAddress();
- } catch (java.net.UnknownHostException e) {
- return "127.0.0.1";
- }
- }
-
- /** The parent HTTP client helper. */
- private volatile HttpClientHelper helper;
-
- /**
- * Constructor setting the request address to the local host.
- *
- * @param helper The parent HTTP client helper.
- * @param method The method name.
- * @param requestUri The request URI.
- */
- protected ClientCall(HttpClientHelper helper, String method, String requestUri) {
- this.helper = helper;
- setMethod(method);
- setRequestUri(requestUri);
- setClientAddress(getLocalAddress());
- }
-
- /**
- * Returns the content length of the request entity if know, {@link Representation#UNKNOWN_SIZE}
- * otherwise.
- *
- * @return The request content length.
- */
- protected long getContentLength() {
- return HeaderUtils.getContentLength(getResponseHeaders());
- }
-
- /**
- * Returns the HTTP client helper.
- *
- * @return The HTTP client helper.
- */
- public HttpClientHelper getHelper() {
- return this.helper;
- }
-
- /**
- * Returns the request entity stream if it exists.
- *
- * @return The request entity stream if it exists.
- */
- public abstract OutputStream getRequestEntityStream();
-
- /**
- * Returns the request head stream if it exists.
- *
- * @return The request head stream if it exists.
- */
- public abstract OutputStream getRequestHeadStream();
-
- /**
- * Returns the response entity if available. Note that no metadata is associated by default, you
- * have to manually set them from your headers.
- *
- * @param response the Response to get the entity from
- * @return The response entity if available.
- */
- public Representation getResponseEntity(Response response) {
- Representation result = null;
- final long size;
-
- // Compute the content length
- Series responseHeaders = getResponseHeaders();
- String transferEncoding =
- responseHeaders.getFirstValue(HeaderConstants.HEADER_TRANSFER_ENCODING, true);
- if ((transferEncoding != null) && !IDENTITY.getName().equalsIgnoreCase(transferEncoding)) {
- size = UNKNOWN_SIZE;
- } else {
- size = getContentLength();
- }
-
- if (!getMethod().equals(Method.HEAD.getName())
- && !response.getStatus().isInformational()
- && !response.getStatus().equals(REDIRECTION_NOT_MODIFIED)
- && !response.getStatus().equals(SUCCESS_NO_CONTENT)
- && !response.getStatus().equals(SUCCESS_RESET_CONTENT)) {
- // Make sure that an InputRepresentation will not be instantiated
- // while the stream is closed.
- InputStream stream = getUnClosedResponseEntityStream(getResponseEntityStream(size));
-
- if (stream != null) {
- result = getRepresentation(stream);
- }
- }
-
- if (result != null) {
- result.setSize(size);
-
- // Informs that the size has not been specified in the header.
- if (size == UNKNOWN_SIZE) {
- getLogger()
- .fine(
- "The length of the message body is unknown. The entity must be handled carefully and consumed entirely to surely release the connection.");
- }
- }
- result = HeaderUtils.extractEntityHeaders(responseHeaders, result);
-
- return result;
- }
-
- /**
- * Returns the response entity stream if it exists.
- *
- * @param size The expected entity size or -1 if unknown.
- * @return The response entity stream if it exists.
- */
- public abstract InputStream getResponseEntityStream(long size);
-
- /**
- * Checks if the given input stream really contains bytes to be read. If so, returns the
- * inputStream otherwise returns null.
- *
- * @param inputStream the inputStream to check.
- * @return null if the given inputStream does not contain any byte, an inputStream otherwise.
- */
- private InputStream getUnClosedResponseEntityStream(InputStream inputStream) {
- InputStream result = null;
-
- if (inputStream != null) {
- try {
- if (inputStream.available() > 0) {
- result = inputStream;
- } else {
- java.io.PushbackInputStream is = new java.io.PushbackInputStream(inputStream);
- int i = is.read();
-
- if (i >= 0) {
- is.unread(i);
- result = is;
- }
- }
- } catch (IOException ioe) {
- getLogger().log(Level.FINER, "End of response entity stream.", ioe);
- }
- }
-
- return result;
- }
-
- @Override
- protected boolean isClientKeepAlive() {
- return true;
- }
-
- @Override
- protected boolean isServerKeepAlive() {
- return !HeaderUtils.isConnectionClose(getResponseHeaders());
- }
-
- /**
- * Sends the request to the client. Commits the request line, headers and optional entity and
- * send them over the network.
- *
- * @param request The high-level request.
- * @return the status of the communication
- */
- public Status sendRequest(Request request) {
- Status result = null;
- Representation entity = request.isEntityAvailable() ? request.getEntity() : null;
-
- // Get the connector service to callback
- org.restlet.service.ConnectorService connectorService =
- ConnectorHelper.getConnectorService();
- if (connectorService != null) {
- connectorService.beforeSend(entity);
- }
-
- try {
- if (entity != null) {
-
- // To work around bug #6472250
- // (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6472250),
- // it is very important to reuse that exact same "requestStream"
- // reference when manipulating the request stream, otherwise
- // "insufficient data sent" exceptions will occur in
- // "fixedLengthMode"
- OutputStream requestStream = getRequestEntityStream();
-
- if (requestStream != null) {
- entity.write(requestStream);
- requestStream.flush();
- requestStream.close();
- }
- }
-
- // Now we can access the status code, this MUST happen after closing
- // any open request stream.
- result = new Status(getStatusCode(), getReasonPhrase());
- } catch (IOException ioe) {
- getHelper()
- .getLogger()
- .log(
- Level.FINE,
- "An error occurred during the communication with the remote HTTP server.",
- ioe);
- result = new Status(CONNECTOR_ERROR_COMMUNICATION, ioe);
- } finally {
- if (entity != null) {
- entity.release();
- }
-
- // Call-back after writing
- if (connectorService != null) {
- connectorService.afterSend(entity);
- }
- }
-
- return result;
- }
-
- /**
- * Sends the request to the client. Commits the request line, headers, and optional entity and
- * send them over the network.
- *
- * @param request The high-level request.
- * @param response The high-level response.
- * @param callback The callback invoked upon request completion.
- */
- public void sendRequest(Request request, Response response, org.restlet.Uniform callback) {
- Context.getCurrentLogger()
- .warning("Currently callbacks are not available for this connector.");
- }
-
- /**
- * Indicates if the request entity should be chunked.
- *
- * @return True if the request should be chunked
- */
- protected boolean shouldRequestBeChunked(Request request) {
- return request.isEntityAvailable()
- && (request.getEntity() != null)
- && !request.getEntity().hasKnownSize();
- }
-}
diff --git a/org.restlet/src/main/java/org/restlet/engine/adapter/HttpClientHelper.java b/org.restlet/src/main/java/org/restlet/engine/adapter/HttpClientHelper.java
deleted file mode 100644
index 7d78746986..0000000000
--- a/org.restlet/src/main/java/org/restlet/engine/adapter/HttpClientHelper.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/**
- * Copyright 2005-2026 Qlik
- *
- * The content of this file is subject to the terms of the Apache 2.0 open
- * source license available at https://www.opensource.org/licenses/apache-2.0
- *
- * Restlet is a registered trademark of QlikTech International AB.
- */
-package org.restlet.engine.adapter;
-
-import java.util.logging.Level;
-import org.restlet.Client;
-import org.restlet.Context;
-import org.restlet.Request;
-import org.restlet.Response;
-import org.restlet.data.Status;
-import org.restlet.engine.connector.ClientHelper;
-
-/**
- * Base HTTP client connector. Here is the list of parameters that are supported. They should be set
- * in the Client's context before it is started:
- *
- *
- * list of supported parameters
- *
- * | Parameter name |
- * Value type |
- * Default value |
- * Description |
- *
- *
- * | adapter |
- * String |
- * org.restlet.engine.adapter.ClientAdapter |
- * Class name of the adapter of low-level HTTP calls into high level
- * requests and responses. |
- *
- *
- *
- * @author Jerome Louvel
- */
-public abstract class HttpClientHelper extends ClientHelper {
-
- /** The adapter from uniform calls to HTTP calls. */
- private volatile ClientAdapter adapter;
-
- /**
- * Constructor.
- *
- * @param client The client to help.
- */
- protected HttpClientHelper(Client client) {
- super(client);
- this.adapter = null;
- }
-
- /**
- * Creates a low-level HTTP client call from a high-level request.
- *
- * @param request The high-level request.
- * @return A low-level HTTP client call.
- */
- public abstract ClientCall create(Request request);
-
- /**
- * Returns the adapter from uniform calls to HTTP calls.
- *
- * @return the adapter from uniform calls to HTTP calls.
- */
- public ClientAdapter getAdapter() throws Exception {
- if (this.adapter == null) {
- String adapterClass =
- getHelpedParameters()
- .getFirstValue("adapter", "org.restlet.engine.adapter.ClientAdapter");
- this.adapter =
- (ClientAdapter)
- Class.forName(adapterClass)
- .getConstructor(Context.class)
- .newInstance(getContext());
- }
-
- return this.adapter;
- }
-
- /**
- * Returns the connection timeout. Defaults to 15000.
- *
- * @return The connection timeout.
- */
- public int getSocketConnectTimeoutMs() {
- return Integer.parseInt(
- getHelpedParameters().getFirstValue("socketConnectTimeoutMs", "15000"));
- }
-
- @Override
- public void handle(Request request, Response response) {
- try {
- ClientCall clientCall = getAdapter().toSpecific(this, request);
- getAdapter().commit(clientCall, request, response);
- } catch (Exception e) {
- getLogger().log(Level.INFO, "Error while handling an HTTP client call", e);
- response.setStatus(Status.CONNECTOR_ERROR_INTERNAL, e);
- }
- }
-
- /**
- * Sets the adapter from uniform calls to HTTP calls.
- *
- * @param adapter The adapter to set.
- */
- public void setAdapter(ClientAdapter adapter) {
- this.adapter = adapter;
- }
-}
diff --git a/org.restlet/src/main/java/org/restlet/engine/adapter/HttpRequest.java b/org.restlet/src/main/java/org/restlet/engine/adapter/HttpRequest.java
deleted file mode 100644
index f80427d230..0000000000
--- a/org.restlet/src/main/java/org/restlet/engine/adapter/HttpRequest.java
+++ /dev/null
@@ -1,644 +0,0 @@
-/**
- * Copyright 2005-2026 Qlik
- *
- * The content of this file is subject to the terms of the Apache 2.0 open
- * source license available at https://www.opensource.org/licenses/apache-2.0
- *
- * Restlet is a registered trademark of QlikTech International AB.
- */
-package org.restlet.engine.adapter;
-
-import static org.restlet.engine.header.HeaderConstants.HEADER_IF_MATCH;
-import static org.restlet.engine.header.HeaderConstants.HEADER_IF_MODIFIED_SINCE;
-import static org.restlet.engine.header.HeaderConstants.HEADER_IF_NONE_MATCH;
-import static org.restlet.engine.header.HeaderConstants.HEADER_IF_RANGE;
-import static org.restlet.engine.header.HeaderConstants.HEADER_IF_UNMODIFIED_SINCE;
-
-import java.io.IOException;
-import java.util.Date;
-import java.util.List;
-import java.util.Set;
-import java.util.logging.Level;
-import org.restlet.Context;
-import org.restlet.Request;
-import org.restlet.data.CacheDirective;
-import org.restlet.data.ChallengeResponse;
-import org.restlet.data.ClientInfo;
-import org.restlet.data.Conditions;
-import org.restlet.data.Cookie;
-import org.restlet.data.Header;
-import org.restlet.data.Method;
-import org.restlet.data.Range;
-import org.restlet.data.RecipientInfo;
-import org.restlet.data.Reference;
-import org.restlet.data.Tag;
-import org.restlet.data.Warning;
-import org.restlet.engine.header.CacheDirectiveReader;
-import org.restlet.engine.header.CookieReader;
-import org.restlet.engine.header.ExpectationReader;
-import org.restlet.engine.header.HeaderConstants;
-import org.restlet.engine.header.HeaderReader;
-import org.restlet.engine.header.PreferenceReader;
-import org.restlet.engine.header.RangeReader;
-import org.restlet.engine.header.RecipientInfoReader;
-import org.restlet.engine.header.StringReader;
-import org.restlet.engine.header.TagReader;
-import org.restlet.engine.header.WarningReader;
-import org.restlet.engine.security.AuthenticatorUtils;
-import org.restlet.engine.util.DateUtils;
-import org.restlet.engine.util.ReferenceUtils;
-import org.restlet.engine.util.StringUtils;
-import org.restlet.representation.Representation;
-import org.restlet.util.Series;
-
-/**
- * Request wrapper for server HTTP calls.
- *
- * @author Jerome Louvel
- */
-public class HttpRequest extends Request {
- /** Indicates if the access control data for request headers was parsed and added */
- private volatile boolean accessControlRequestHeadersAdded;
-
- /** Indicates if the access control data for request methods was parsed and added */
- private volatile boolean accessControlRequestMethodAdded;
-
- /** Indicates if the cache control data was parsed and added. */
- private volatile boolean cacheDirectivesAdded;
-
- /** Indicates if the client data was parsed and added. */
- private volatile boolean clientAdded;
-
- /** Indicates if the conditions were parsed and added. */
- private volatile boolean conditionAdded;
-
- /** The context of the HTTP server connector that issued the call. */
- private volatile Context context;
-
- /** Indicates if the cookies were parsed and added. */
- private volatile boolean cookiesAdded;
-
- /** Indicates if the request entity was added. */
- private volatile boolean entityAdded;
-
- /** The low-level HTTP call. */
- private volatile ServerCall httpCall;
-
- /** Indicates if the proxy security data was parsed and added. */
- private volatile boolean proxySecurityAdded;
-
- /** Indicates if the ranges data was parsed and added. */
- private volatile boolean rangesAdded;
-
- /** Indicates if the recipients info was parsed and added. */
- private volatile boolean recipientsInfoAdded;
-
- /** Indicates if the referrer was parsed and added. */
- private volatile boolean referrerAdded;
-
- /** Indicates if the security data was parsed and added. */
- private volatile boolean securityAdded;
-
- /** Indicates if the warning data was parsed and added. */
- private volatile boolean warningsAdded;
-
- /**
- * Constructor.
- *
- * @param context The context of the HTTP server connector that issued the call.
- * @param httpCall The low-level HTTP server call.
- */
- public HttpRequest(Context context, ServerCall httpCall) {
- this.context = context;
- this.clientAdded = false;
- this.conditionAdded = false;
- this.cookiesAdded = false;
- this.entityAdded = false;
- this.referrerAdded = false;
- this.securityAdded = false;
- this.proxySecurityAdded = false;
- this.recipientsInfoAdded = false;
- this.warningsAdded = false;
- this.httpCall = httpCall;
-
- // Set the properties
- setMethod(Method.valueOf(httpCall.getMethod()));
-
- // Set the host reference
- StringBuilder sb = new StringBuilder();
- sb.append(httpCall.getProtocol().getSchemeName()).append("://");
- sb.append(httpCall.getHostDomain());
- if ((httpCall.getHostPort() != -1)
- && (httpCall.getHostPort() != httpCall.getProtocol().getDefaultPort())) {
- sb.append(':').append(httpCall.getHostPort());
- }
- setHostRef(sb.toString());
-
- // Set the resource reference
- if (httpCall.getRequestUri() != null) {
- setResourceRef(new Reference(getHostRef(), httpCall.getRequestUri()));
-
- if (getResourceRef().isRelative()) {
- // Take care of the "/" between the host part and the segments.
- if (!httpCall.getRequestUri().startsWith("/")) {
- setResourceRef(
- new Reference(
- getHostRef().toString() + "/" + httpCall.getRequestUri()));
- } else {
- setResourceRef(
- new Reference(getHostRef().toString() + httpCall.getRequestUri()));
- }
- }
-
- setOriginalRef(
- ReferenceUtils.getOriginalRef(getResourceRef(), httpCall.getRequestHeaders()));
- }
-
- // Set the request date
- String dateHeader =
- httpCall.getRequestHeaders().getFirstValue(HeaderConstants.HEADER_DATE, true);
- Date date = null;
- if (dateHeader != null) {
- date = DateUtils.parse(dateHeader);
- }
-
- if (date == null) {
- date = new Date();
- }
-
- setDate(date);
- }
-
- @Override
- public boolean abort() {
- return getHttpCall().abort();
- }
-
- @Override
- public void flushBuffers() throws IOException {
- getHttpCall().flushBuffers();
- }
-
- @Override
- public Set getAccessControlRequestHeaders() {
- Set result = super.getAccessControlRequestHeaders();
- if (!accessControlRequestHeadersAdded) {
- for (String header :
- getHttpCall()
- .getRequestHeaders()
- .getValuesArray(
- HeaderConstants.HEADER_ACCESS_CONTROL_REQUEST_HEADERS, true)) {
- new StringReader(header).addValues(result);
- }
- accessControlRequestHeadersAdded = true;
- }
- return result;
- }
-
- @Override
- public Method getAccessControlRequestMethod() {
- Method result = super.getAccessControlRequestMethod();
- if (!accessControlRequestMethodAdded) {
- String header =
- getHttpCall()
- .getRequestHeaders()
- .getFirstValue(
- HeaderConstants.HEADER_ACCESS_CONTROL_REQUEST_METHOD, true);
- if (header != null) {
- result = Method.valueOf(header);
- super.setAccessControlRequestMethod(result);
- }
- accessControlRequestMethodAdded = true;
- }
- return result;
- }
-
- @Override
- public List getCacheDirectives() {
- List result = super.getCacheDirectives();
-
- if (!cacheDirectivesAdded) {
- for (Header header :
- getHttpCall()
- .getRequestHeaders()
- .subList(HeaderConstants.HEADER_CACHE_CONTROL)) {
- CacheDirectiveReader.addValues(header, result);
- }
-
- cacheDirectivesAdded = true;
- }
-
- return result;
- }
-
- @Override
- public ChallengeResponse getChallengeResponse() {
- ChallengeResponse result = super.getChallengeResponse();
-
- if (!this.securityAdded) {
- // Extract the header value
- String authorization =
- getHttpCall()
- .getRequestHeaders()
- .getValues(HeaderConstants.HEADER_AUTHORIZATION);
-
- // Set the challenge response
- result =
- AuthenticatorUtils.parseResponse(
- this, authorization, getHttpCall().getRequestHeaders());
- setChallengeResponse(result);
- this.securityAdded = true;
- }
-
- return result;
- }
-
- /**
- * Returns the client-specific information.
- *
- * @return The client-specific information.
- */
- @Override
- public ClientInfo getClientInfo() {
- final ClientInfo result = super.getClientInfo();
-
- if (this.clientAdded) {
- return result;
- }
-
- extractPreferences(result);
-
- // Set other properties
- result.setAgent(
- getHttpCall().getRequestHeaders().getValues(HeaderConstants.HEADER_USER_AGENT));
- result.setFrom(
- getHttpCall().getRequestHeaders().getFirstValue(HeaderConstants.HEADER_FROM, true));
- result.setAddress(getHttpCall().getClientAddress());
- result.setPort(getHttpCall().getClientPort());
-
- if (getHttpCall().getUserPrincipal() != null) {
- result.getPrincipals().add(getHttpCall().getUserPrincipal());
- }
-
- if (this.context != null) {
- // Special handling for the non-standard but common
- // "X-Forwarded-For" header.
- final boolean useForwardedForHeader =
- Boolean.parseBoolean(
- this.context
- .getParameters()
- .getFirstValue("useForwardedForHeader", false));
- if (useForwardedForHeader) {
- // Lookup the "X-Forwarded-For" header supported by popular
- // proxies and caches.
- final String header =
- getHttpCall()
- .getRequestHeaders()
- .getValues(HeaderConstants.HEADER_X_FORWARDED_FOR);
- if (header != null) {
- final String[] addresses = header.split(",");
- for (String s : addresses) {
- String address = s.trim();
- result.getForwardedAddresses().add(address);
- }
- }
- }
- }
-
- this.clientAdded = true;
-
- return result;
- }
-
- private void extractPreferences(final ClientInfo result) {
- // Parse the Accept* headers. If an error occurs during the parsing
- // of each header, the error is traced, and we keep on with the other
- // headers.
- try {
- String acceptCharset =
- getHttpCall()
- .getRequestHeaders()
- .getValues(HeaderConstants.HEADER_ACCEPT_CHARSET);
- PreferenceReader.addCharacterSets(acceptCharset, result);
- } catch (Exception e) {
- this.context.getLogger().log(Level.INFO, e.getMessage());
- }
-
- try {
- String acceptEncoding =
- getHttpCall()
- .getRequestHeaders()
- .getValues(HeaderConstants.HEADER_ACCEPT_ENCODING);
- PreferenceReader.addEncodings(acceptEncoding, result);
- } catch (Exception e) {
- this.context.getLogger().log(Level.INFO, e.getMessage());
- }
-
- try {
- String acceptLanguage =
- getHttpCall()
- .getRequestHeaders()
- .getValues(HeaderConstants.HEADER_ACCEPT_LANGUAGE);
- PreferenceReader.addLanguages(acceptLanguage, result);
- } catch (Exception e) {
- this.context.getLogger().log(Level.INFO, e.getMessage());
- }
-
- try {
- String acceptMediaType =
- getHttpCall().getRequestHeaders().getValues(HeaderConstants.HEADER_ACCEPT);
- PreferenceReader.addMediaTypes(acceptMediaType, result);
- } catch (Exception e) {
- this.context.getLogger().log(Level.INFO, e.getMessage());
- }
-
- try {
- String acceptPatch =
- getHttpCall()
- .getRequestHeaders()
- .getValues(HeaderConstants.HEADER_ACCEPT_PATCH);
- PreferenceReader.addPatches(acceptPatch, result);
- } catch (Exception e) {
- this.context.getLogger().log(Level.INFO, e.getMessage());
- }
-
- try {
- String expect =
- getHttpCall().getRequestHeaders().getValues(HeaderConstants.HEADER_EXPECT);
- ExpectationReader.addValues(expect, result);
- } catch (Exception e) {
- this.context.getLogger().log(Level.INFO, e.getMessage());
- }
- }
-
- /**
- * Returns the condition data applying to this call.
- *
- * @return The condition data applying to this call.
- */
- @Override
- public Conditions getConditions() {
- final Conditions result = super.getConditions();
-
- if (this.conditionAdded) {
- return result;
- }
-
- // Extract the header values
- Date ifModifiedSince = null;
- Date ifUnmodifiedSince = null;
-
- for (Header header : getHttpCall().getRequestHeaders()) {
- if (header.getName().equalsIgnoreCase(HEADER_IF_MODIFIED_SINCE)) {
- ifModifiedSince = HeaderReader.readDate(header.getValue(), false);
- } else if (header.getName().equalsIgnoreCase(HEADER_IF_UNMODIFIED_SINCE)) {
- ifUnmodifiedSince = HeaderReader.readDate(header.getValue(), false);
- }
- }
-
- // Set the If-Modified-Since date
- if ((ifModifiedSince != null) && (ifModifiedSince.getTime() != -1)) {
- result.setModifiedSince(ifModifiedSince);
- }
-
- // Set the If-Unmodified-Since date
- if ((ifUnmodifiedSince != null) && (ifUnmodifiedSince.getTime() != -1)) {
- result.setUnmodifiedSince(ifUnmodifiedSince);
- }
-
- extractIfMatch(result);
- extractIfNoneMatch(result);
- extractIfRangeCondition(result);
-
- this.conditionAdded = true;
-
- return result;
- }
-
- private void extractIfNoneMatch(final Conditions result) {
- // Set the If-None-Match tags
- String ifNoneMatchHeader =
- getHttpCall().getRequestHeaders().getValues(HEADER_IF_NONE_MATCH);
- if (ifNoneMatchHeader != null) {
- try {
- new TagReader(ifNoneMatchHeader).addValues(result.getNoneMatch());
- } catch (Exception e) {
- this.context
- .getLogger()
- .log(
- Level.INFO,
- "Unable to process the if-none-match header: {0}",
- ifNoneMatchHeader);
- }
- }
- }
-
- private void extractIfMatch(final Conditions result) {
- // Set the If-Match tags
- String ifMatchHeader = getHttpCall().getRequestHeaders().getValues(HEADER_IF_MATCH);
- if (ifMatchHeader != null) {
- try {
- new TagReader(ifMatchHeader).addValues(result.getMatch());
- } catch (Exception e) {
- this.context
- .getLogger()
- .log(
- Level.INFO,
- "Unable to process the if-match header: {0}",
- ifMatchHeader);
- }
- }
- }
-
- private void extractIfRangeCondition(final Conditions result) {
- String ifRangeHeader =
- getHttpCall().getRequestHeaders().getFirstValue(HEADER_IF_RANGE, true);
- if (!StringUtils.isNullOrEmpty(ifRangeHeader)) {
- Tag tag = Tag.parse(ifRangeHeader);
-
- if (tag != null) {
- result.setRangeTag(tag);
- } else {
- Date date = HeaderReader.readDate(ifRangeHeader, false);
- result.setRangeDate(date);
- }
- }
- }
-
- /**
- * Returns the cookies provided by the client.
- *
- * @return The cookies provided by the client.
- */
- @Override
- public Series getCookies() {
- Series result = super.getCookies();
-
- if (!this.cookiesAdded) {
- String cookieValues =
- getHttpCall().getRequestHeaders().getValues(HeaderConstants.HEADER_COOKIE);
-
- if (cookieValues != null) {
- new CookieReader(cookieValues).addValues(result);
- }
-
- this.cookiesAdded = true;
- }
-
- return result;
- }
-
- /**
- * Returns the representation provided by the client.
- *
- * @return The representation provided by the client.
- */
- @Override
- public Representation getEntity() {
- if (!this.entityAdded) {
- setEntity(getHttpCall().getRequestEntity());
- this.entityAdded = true;
- }
-
- return super.getEntity();
- }
-
- /**
- * Returns the low-level HTTP call.
- *
- * @return The low-level HTTP call.
- */
- public ServerCall getHttpCall() {
- return this.httpCall;
- }
-
- @Override
- public ChallengeResponse getProxyChallengeResponse() {
- ChallengeResponse result = super.getProxyChallengeResponse();
-
- if (!this.proxySecurityAdded) {
- // Extract the header value
- final String authorization =
- getHttpCall()
- .getRequestHeaders()
- .getValues(HeaderConstants.HEADER_PROXY_AUTHORIZATION);
-
- // Set the challenge response
- result =
- AuthenticatorUtils.parseResponse(
- this, authorization, getHttpCall().getRequestHeaders());
- setProxyChallengeResponse(result);
- this.proxySecurityAdded = true;
- }
-
- return result;
- }
-
- @Override
- public List getRanges() {
- final List result = super.getRanges();
-
- if (!this.rangesAdded) {
- // Extract the header value
- final String ranges =
- getHttpCall().getRequestHeaders().getValues(HeaderConstants.HEADER_RANGE);
- result.addAll(RangeReader.read(ranges));
-
- this.rangesAdded = true;
- }
-
- return result;
- }
-
- @Override
- public List getRecipientsInfo() {
- List result = super.getRecipientsInfo();
- if (!recipientsInfoAdded) {
- for (String header :
- getHttpCall()
- .getRequestHeaders()
- .getValuesArray(HeaderConstants.HEADER_VIA, true)) {
- new RecipientInfoReader(header).addValues(result);
- }
- recipientsInfoAdded = true;
- }
- return result;
- }
-
- /**
- * Returns the referrer reference if available.
- *
- * @return The referrer reference.
- */
- @Override
- public Reference getReferrerRef() {
- if (!this.referrerAdded) {
- final String referrerValue =
- getHttpCall().getRequestHeaders().getValues(HeaderConstants.HEADER_REFERRER);
- if (referrerValue != null) {
- setReferrerRef(new Reference(referrerValue));
- }
-
- this.referrerAdded = true;
- }
-
- return super.getReferrerRef();
- }
-
- @Override
- public List getWarnings() {
- List result = super.getWarnings();
- if (!warningsAdded) {
- for (String header :
- getHttpCall()
- .getRequestHeaders()
- .getValuesArray(HeaderConstants.HEADER_WARNING, true)) {
- new WarningReader(header).addValues(result);
- }
- warningsAdded = true;
- }
- return result;
- }
-
- @Override
- public void setAccessControlRequestHeaders(Set accessControlRequestHeaders) {
- super.setAccessControlRequestHeaders(accessControlRequestHeaders);
- this.accessControlRequestHeadersAdded = true;
- }
-
- @Override
- public void setAccessControlRequestMethod(Method accessControlRequestMethod) {
- super.setAccessControlRequestMethod(accessControlRequestMethod);
- this.accessControlRequestMethodAdded = true;
- }
-
- @Override
- public void setChallengeResponse(ChallengeResponse response) {
- super.setChallengeResponse(response);
- this.securityAdded = true;
- }
-
- @Override
- public void setEntity(Representation entity) {
- super.setEntity(entity);
- this.entityAdded = true;
- }
-
- @Override
- public void setProxyChallengeResponse(ChallengeResponse response) {
- super.setProxyChallengeResponse(response);
- this.proxySecurityAdded = true;
- }
-
- @Override
- public void setRecipientsInfo(List recipientsInfo) {
- super.setRecipientsInfo(recipientsInfo);
- this.recipientsInfoAdded = true;
- }
-
- @Override
- public void setWarnings(List warnings) {
- super.setWarnings(warnings);
- this.warningsAdded = true;
- }
-}
diff --git a/org.restlet/src/main/java/org/restlet/engine/adapter/HttpResponse.java b/org.restlet/src/main/java/org/restlet/engine/adapter/HttpResponse.java
deleted file mode 100644
index c47b271fb0..0000000000
--- a/org.restlet/src/main/java/org/restlet/engine/adapter/HttpResponse.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * Copyright 2005-2026 Qlik
- *
- * The content of this file is subject to the terms of the Apache 2.0 open
- * source license available at https://www.opensource.org/licenses/apache-2.0
- *
- * Restlet is a registered trademark of QlikTech International AB.
- */
-package org.restlet.engine.adapter;
-
-import org.restlet.Request;
-import org.restlet.Response;
-import org.restlet.data.ServerInfo;
-import org.restlet.data.Status;
-import org.restlet.engine.Engine;
-
-/**
- * Response wrapper for server HTTP calls.
- *
- * @author Jerome Louvel
- */
-public class HttpResponse extends Response {
- /**
- * Adds a new header to the given request.
- *
- * @param response The response to modify.
- * @param headerName The header name to add.
- * @param headerValue The header value to add.
- */
- public static void addHeader(Response response, String headerName, String headerValue) {
- if (response instanceof HttpResponse) {
- response.getHeaders().add(headerName, headerValue);
- }
- }
-
- /** The low-level HTTP call. */
- private volatile ServerCall httpCall;
-
- /** Indicates if the server data was parsed and added. */
- private volatile boolean serverAdded;
-
- /**
- * Constructor.
- *
- * @param httpCall The low-level HTTP server call.
- * @param request The associated high-level request.
- */
- public HttpResponse(ServerCall httpCall, Request request) {
- super(request);
- this.serverAdded = false;
- this.httpCall = httpCall;
-
- // Set the properties
- setStatus(Status.SUCCESS_OK);
- }
-
- /**
- * Returns the low-level HTTP call.
- *
- * @return The low-level HTTP call.
- */
- public ServerCall getHttpCall() {
- return this.httpCall;
- }
-
- /**
- * Returns the server-specific information.
- *
- * @return The server-specific information.
- */
- @Override
- public ServerInfo getServerInfo() {
- final ServerInfo result = super.getServerInfo();
-
- if (!this.serverAdded) {
- result.setAddress(this.httpCall.getServerAddress());
- result.setAgent(Engine.VERSION_HEADER);
- result.setPort(this.httpCall.getServerPort());
- this.serverAdded = true;
- }
-
- return result;
- }
-}
diff --git a/org.restlet/src/main/java/org/restlet/engine/adapter/HttpServerHelper.java b/org.restlet/src/main/java/org/restlet/engine/adapter/HttpServerHelper.java
deleted file mode 100644
index bb2a032d9c..0000000000
--- a/org.restlet/src/main/java/org/restlet/engine/adapter/HttpServerHelper.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/**
- * Copyright 2005-2026 Qlik
- *
- * The content of this file is subject to the terms of the Apache 2.0 open
- * source license available at https://www.opensource.org/licenses/apache-2.0
- *
- * Restlet is a registered trademark of QlikTech International AB.
- */
-package org.restlet.engine.adapter;
-
-import java.lang.reflect.InvocationTargetException;
-import java.util.logging.Level;
-import org.restlet.Context;
-import org.restlet.Server;
-import org.restlet.engine.Engine;
-import org.restlet.engine.connector.ServerHelper;
-
-/**
- * Base HTTP server connector. Here is the list of parameters that are supported. They should be set
- * in the Server's context before it is started:
- *
- *
- * list of supported parameters
- *
- * | Parameter name |
- * Value type |
- * Default value |
- * Description |
- *
- *
- * | useForwardedForHeader |
- * boolean |
- * false |
- * Lookup the "X-Forwarded-For" header supported by popular proxies and
- * caches and uses it to populate the Request.getClientAddresses() method
- * result. This information is only safe for intermediary components within your
- * local network. Other addresses could easily be changed by setting a fake
- * header and should not be trusted for serious security checks. |
- *
- *
- * | adapter |
- * String |
- * org.restlet.engine.adapter.ServerAdapter |
- * Class name of the adapter of low-level HTTP calls into high level
- * requests and responses. |
- *
- *
- *
- * @author Jerome Louvel
- */
-public class HttpServerHelper extends ServerHelper {
-
- /** The adapter from HTTP calls to uniform calls. */
- private volatile ServerAdapter adapter;
-
- /**
- * Default constructor. Note that many methods assume that a non-null server is set to work
- * properly. You can use the setHelped(Server) method for this purpose or better rely on the
- * other constructor.
- */
- public HttpServerHelper() {
- this(null);
- }
-
- /**
- * Constructor.
- *
- * @param server The server to help.
- */
- public HttpServerHelper(Server server) {
- super(server);
- this.adapter = null;
- }
-
- /**
- * Returns the adapter from HTTP calls to uniform calls.
- *
- * @return the adapter from HTTP calls to uniform calls.
- */
- public ServerAdapter getAdapter() {
- if (this.adapter == null) {
- try {
- final String adapterClass =
- getHelpedParameters()
- .getFirstValue(
- "adapter", "org.restlet.engine.adapter.ServerAdapter");
- this.adapter =
- (ServerAdapter)
- Engine.loadClass(adapterClass)
- .getConstructor(Context.class)
- .newInstance(getContext());
- } catch (IllegalArgumentException
- | SecurityException
- | InstantiationException
- | IllegalAccessException
- | InvocationTargetException
- | NoSuchMethodException
- | ClassNotFoundException e) {
- getLogger().log(Level.SEVERE, "Unable to create the HTTP server adapter", e);
- }
- }
-
- return this.adapter;
- }
-
- /**
- * Handles the connector call. The default behavior is to create an REST call and delegate it to
- * the attached Restlet.
- *
- * @param httpCall The HTTP server call.
- */
- public void handle(ServerCall httpCall) {
- try {
- HttpRequest request = getAdapter().toRequest(httpCall);
- HttpResponse response = new HttpResponse(httpCall, request);
- handle(request, response);
- getAdapter().commit(response);
- } catch (Exception e) {
- getLogger().log(Level.WARNING, "Error while handling an HTTP server call", e);
- } finally {
- Engine.clearThreadLocalVariables();
- }
- }
-
- /**
- * Sets the adapter from HTTP calls to uniform calls.
- *
- * @param adapter The converter to set.
- */
- public void setAdapter(ServerAdapter adapter) {
- this.adapter = adapter;
- }
-}
diff --git a/org.restlet/src/main/java/org/restlet/engine/adapter/ServerAdapter.java b/org.restlet/src/main/java/org/restlet/engine/adapter/ServerAdapter.java
deleted file mode 100644
index d1426f60a2..0000000000
--- a/org.restlet/src/main/java/org/restlet/engine/adapter/ServerAdapter.java
+++ /dev/null
@@ -1,259 +0,0 @@
-/**
- * Copyright 2005-2026 Qlik
- *
- * The content of this file is subject to the terms of the Apache 2.0 open
- * source license available at https://www.opensource.org/licenses/apache-2.0
- *
- * Restlet is a registered trademark of QlikTech International AB.
- */
-package org.restlet.engine.adapter;
-
-import static org.restlet.engine.header.HeaderConstants.ATTRIBUTE_HEADERS;
-import static org.restlet.engine.header.HeaderConstants.ATTRIBUTE_HTTPS_KEY_SIZE;
-import static org.restlet.engine.header.HeaderConstants.ATTRIBUTE_HTTPS_SSL_SESSION_ID;
-import static org.restlet.engine.header.HeaderConstants.ATTRIBUTE_VERSION;
-
-import java.io.IOException;
-import java.security.cert.Certificate;
-import java.util.List;
-import java.util.logging.Level;
-import org.restlet.Context;
-import org.restlet.data.Header;
-import org.restlet.data.Method;
-import org.restlet.data.Status;
-import org.restlet.engine.header.HeaderUtils;
-import org.restlet.representation.Representation;
-import org.restlet.util.Series;
-
-/**
- * Converter of low-level HTTP server calls into high-level uniform calls.
- *
- * @author Jerome Louvel
- */
-public class ServerAdapter extends Adapter {
-
- /**
- * Constructor.
- *
- * @param context The client context.
- */
- public ServerAdapter(Context context) {
- super(context);
- }
-
- /**
- * Adds the entity headers for the handled uniform call.
- *
- * @param response The response returned.
- */
- protected void addEntityHeaders(HttpResponse response) {
- Series responseHeaders = response.getHttpCall().getResponseHeaders();
- Representation entity = response.getEntity();
- HeaderUtils.addEntityHeaders(entity, responseHeaders);
- }
-
- /**
- * Adds the response headers for the handled uniform call.
- *
- * @param response The response returned.
- */
- protected void addResponseHeaders(HttpResponse response) {
- try {
- // Add all the necessary headers
- HeaderUtils.addGeneralHeaders(response, response.getHttpCall().getResponseHeaders());
- HeaderUtils.addResponseHeaders(response, response.getHttpCall().getResponseHeaders());
-
- // Set the status code in the response
- if (response.getStatus() != null) {
- response.getHttpCall().setStatusCode(response.getStatus().getCode());
- response.getHttpCall().setReasonPhrase(response.getStatus().getReasonPhrase());
- }
- } catch (Exception e) {
- getLogger()
- .log(
- Level.WARNING,
- "Exception intercepted while adding the response headers",
- e);
- response.getHttpCall().setStatusCode(Status.SERVER_ERROR_INTERNAL.getCode());
- response.getHttpCall().setReasonPhrase(Status.SERVER_ERROR_INTERNAL.getReasonPhrase());
- }
- }
-
- /**
- * Commits the changes to a handled uniform call back into the original HTTP call. The default
- * implementation first invokes the "addResponseHeaders" then asks the "htppCall" to send the
- * response back to the client.
- *
- * @param response The high-level response.
- */
- public void commit(HttpResponse response) {
- try {
- if (Method.HEAD.equals(response.getRequest().getMethod())) {
- addEntityHeaders(response);
- response.setEntity(null);
- } else if (Method.GET.equals(response.getRequest().getMethod())
- && Status.SUCCESS_OK.equals(response.getStatus())
- && (!response.isEntityAvailable())) {
- addEntityHeaders(response);
- getLogger()
- .warning(
- "A response with a 200 (Ok) status should have an entity. Make sure that resource \""
- + response.getRequest().getResourceRef()
- + "\" returns one or sets the status to 204 (No content).");
- } else if (response.getStatus().equals(Status.SUCCESS_NO_CONTENT)) {
- handleNoContentResponse(response);
- } else if (response.getStatus().equals(Status.SUCCESS_RESET_CONTENT)) {
- handleResetContent(response);
- } else if (response.getStatus().equals(Status.REDIRECTION_NOT_MODIFIED)) {
- handleRedirectionNotNodified(response);
- } else if (response.getStatus().isInformational()) {
- handleInformationalStatus(response);
- } else {
- addEntityHeaders(response);
-
- if (!response.isEntityAvailable()) {
- if ((response.getEntity() != null) && (response.getEntity().getSize() != 0)) {
- getLogger()
- .warning(
- "A response with an unavailable and potentially non empty entity was returned. Ignoring the entity for resource \""
- + response.getRequest().getResourceRef()
- + "\".");
- }
-
- response.setEntity(null);
- }
- }
-
- // Add the response headers
- addResponseHeaders(response);
-
- // Send the response to the client
- response.getHttpCall().sendResponse(response);
- } catch (Exception exception) {
- handleException(response, exception);
- } finally {
- completeResponse(response);
- }
- }
-
- private static void completeResponse(final HttpResponse response) {
- response.getHttpCall().complete();
-
- if (response.getOnSent() != null) {
- response.getOnSent().handle(response.getRequest(), response);
- }
- }
-
- private void handleInformationalStatus(final HttpResponse response) {
- if (response.isEntityAvailable()) {
- getLogger()
- .warning(
- "Responses with an informational (1xx) status can't have an entity. Ignoring the entity for resource \""
- + response.getRequest().getResourceRef()
- + "\".");
- response.setEntity(null);
- }
- }
-
- private static void handleRedirectionNotNodified(final HttpResponse response) {
- if (response.getEntity() != null) {
- HeaderUtils.addNotModifiedEntityHeaders(
- response.getEntity(), response.getHttpCall().getResponseHeaders());
- response.setEntity(null);
- }
- }
-
- private void handleResetContent(final HttpResponse response) {
- if (response.isEntityAvailable()) {
- getLogger()
- .warning(
- "Responses with a 205 (Reset content) status can't have an entity. Ignoring the entity for resource \""
- + response.getRequest().getResourceRef()
- + "\".");
- response.setEntity(null);
- }
- }
-
- private void handleNoContentResponse(final HttpResponse response) {
- addEntityHeaders(response);
-
- if (response.isEntityAvailable()) {
- getLogger()
- .fine(
- "Responses with a 204 (No content) status generally don't have an entity. Only adding entity headers for resource \""
- + response.getRequest().getResourceRef()
- + "\".");
- response.setEntity(null);
- }
- }
-
- private void handleException(final HttpResponse response, final Exception exception) {
- if (response.getHttpCall().isConnectionBroken(exception)) {
- // output a single log line for this common case to avoid filling server logs
- getLogger()
- .log(
- Level.INFO,
- "The connection was broken. It was probably closed by the client. Reason: {0}",
- exception.getMessage());
- } else {
- getLogger()
- .log(
- Level.SEVERE,
- "An exception occurred writing the response entity",
- exception);
- response.getHttpCall().setStatusCode(Status.SERVER_ERROR_INTERNAL.getCode());
- response.getHttpCall()
- .setReasonPhrase("An exception occurred writing the response entity");
- response.setEntity(null);
-
- try {
- response.getHttpCall().sendResponse(response);
- } catch (IOException ioe) {
- getLogger().log(Level.WARNING, "Unable to send error response", ioe);
- }
- }
- }
-
- /**
- * Converts a low-level HTTP call into a high-level uniform request.
- *
- * @param httpCall The low-level HTTP call.
- * @return A new high-level uniform request.
- */
- public HttpRequest toRequest(ServerCall httpCall) {
- HttpRequest result = new HttpRequest(getContext(), httpCall);
- result.getAttributes().put(ATTRIBUTE_HEADERS, httpCall.getRequestHeaders());
-
- if (httpCall.getVersion() != null) {
- result.getAttributes().put(ATTRIBUTE_VERSION, httpCall.getVersion());
- }
-
- if (httpCall.isConfidential()) {
- List clientCertificates = httpCall.getCertificates();
-
- if (clientCertificates != null) {
- result.getClientInfo().setCertificates(clientCertificates);
- }
-
- String cipherSuite = httpCall.getCipherSuite();
-
- if (cipherSuite != null) {
- result.getClientInfo().setCipherSuite(cipherSuite);
- }
-
- Integer keySize = httpCall.getSslKeySize();
-
- if (keySize != null) {
- result.getAttributes().put(ATTRIBUTE_HTTPS_KEY_SIZE, keySize);
- }
-
- String sslSessionId = httpCall.getSslSessionId();
-
- if (sslSessionId != null) {
- result.getAttributes().put(ATTRIBUTE_HTTPS_SSL_SESSION_ID, sslSessionId);
- }
- }
-
- return result;
- }
-}
diff --git a/org.restlet/src/main/java/org/restlet/engine/adapter/ServerCall.java b/org.restlet/src/main/java/org/restlet/engine/adapter/ServerCall.java
deleted file mode 100644
index 3846cdb3fc..0000000000
--- a/org.restlet/src/main/java/org/restlet/engine/adapter/ServerCall.java
+++ /dev/null
@@ -1,538 +0,0 @@
-/**
- * Copyright 2005-2026 Qlik
- *
- * The content of this file is subject to the terms of the Apache 2.0 open
- * source license available at https://www.opensource.org/licenses/apache-2.0
- *
- * Restlet is a registered trademark of QlikTech International AB.
- */
-package org.restlet.engine.adapter;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PushbackInputStream;
-import java.security.cert.Certificate;
-import java.util.Base64;
-import java.util.List;
-import java.util.logging.Level;
-import org.restlet.Context;
-import org.restlet.Response;
-import org.restlet.Server;
-import org.restlet.data.Digest;
-import org.restlet.data.Header;
-import org.restlet.engine.connector.ConnectorHelper;
-import org.restlet.engine.header.ContentType;
-import org.restlet.engine.header.DispositionReader;
-import org.restlet.engine.header.EncodingReader;
-import org.restlet.engine.header.HeaderConstants;
-import org.restlet.engine.header.HeaderReader;
-import org.restlet.engine.header.HeaderUtils;
-import org.restlet.engine.header.LanguageReader;
-import org.restlet.engine.header.RangeReader;
-import org.restlet.engine.io.IoUtils;
-import org.restlet.engine.util.StringUtils;
-import org.restlet.representation.EmptyRepresentation;
-import org.restlet.representation.InputRepresentation;
-import org.restlet.representation.Representation;
-import org.restlet.service.ConnectorService;
-
-/**
- * Abstract HTTP server connector call.
- *
- * @author Jerome Louvel
- */
-public abstract class ServerCall extends Call {
-
- /** Indicates if the "host" header was already parsed. */
- private volatile boolean hostParsed;
-
- /**
- * Constructor.
- *
- * @param server The parent server connector.
- */
- protected ServerCall(Server server) {
- this(
- (server == null) ? null : server.getAddress(),
- (server == null) ? 0 : server.getPort());
- }
-
- /**
- * Constructor.
- *
- * @param serverAddress The server IP address.
- * @param serverPort The server port.
- */
- protected ServerCall(String serverAddress, int serverPort) {
- setServerAddress(serverAddress);
- setServerPort(serverPort);
- this.hostParsed = false;
- }
-
- /**
- * Ask the connector to abort the related network connection, for example immediately closing
- * the socket.
- *
- * @return True if the connection was aborted.
- */
- public abstract boolean abort();
-
- /** Complete the response */
- public void complete() {}
-
- /**
- * Flushes the buffers onto the network so that for example you can force headers to be written
- * before the entity is becoming available.
- *
- * @throws IOException
- */
- public void flushBuffers() throws IOException {}
-
- /**
- * Returns the chain of client SSL certificates, if available and accessible.
- *
- * @return The chain of client SSL certificates, if available and accessible.
- */
- public List getCertificates() {
- return List.of();
- }
-
- /**
- * Returns the SSL Cipher Suite, if available and accessible.
- *
- * @return The SSL Cipher Suite, if available and accessible.
- */
- public String getCipherSuite() {
- return null;
- }
-
- /**
- * Returns the content length of the request entity if know, {@link Representation#UNKNOWN_SIZE}
- * otherwise.
- *
- * @return The request content length.
- */
- protected long getContentLength() {
- return HeaderUtils.getContentLength(getRequestHeaders());
- }
-
- /**
- * Returns the host domain name.
- *
- * @return The host domain name.
- */
- @Override
- public String getHostDomain() {
- if (!this.hostParsed) {
- parseHost();
- }
- return super.getHostDomain();
- }
-
- /**
- * Returns the host port.
- *
- * @return The host port.
- */
- @Override
- public int getHostPort() {
- if (!this.hostParsed) {
- parseHost();
- }
- return super.getHostPort();
- }
-
- /**
- * Returns the request entity if available.
- *
- * @return The request entity if available.
- */
- public Representation getRequestEntity() {
- Representation result;
- long contentLength = getContentLength();
- boolean chunkedEncoding = HeaderUtils.isChunkedEncoding(getRequestHeaders());
- // In some cases, there is an entity without a content-length header
- boolean connectionClosed = HeaderUtils.isConnectionClose(getRequestHeaders());
-
- // Create the representation
- if (((contentLength != Representation.UNKNOWN_SIZE) && (contentLength != 0))
- || chunkedEncoding
- || connectionClosed) {
- // Create the result representation
- InputStream requestStream = getInputStream(contentLength, connectionClosed);
-
- if (requestStream != null) {
- result = new InputRepresentation(requestStream, null, contentLength);
- } else {
- result = new EmptyRepresentation();
- }
-
- result.setSize(contentLength);
- } else {
- result = new EmptyRepresentation();
- }
-
- // Extract some interesting header values
- extractRequestHeaders(result);
-
- return result;
- }
-
- private InputStream getInputStream(final long contentLength, final boolean connectionClosed) {
- InputStream requestStream = getRequestEntityStream(contentLength);
-
- if (connectionClosed) {
- // We need to detect if there is really an entity or not as only
- // the end of the connection can let us know at this point
- PushbackInputStream pbi = new PushbackInputStream(requestStream);
-
- try {
- int next = pbi.read();
-
- if (next != -1) {
- pbi.unread(next);
- requestStream = pbi;
- } else {
- pbi.close();
- requestStream = null;
- }
- } catch (IOException e) {
- getLogger().fine("Unable to read request entity");
-
- try {
- pbi.close();
- } catch (IOException e1) {
- getLogger().fine("Unable to close request entity");
- }
-
- requestStream = null;
- }
- }
- return requestStream;
- }
-
- private void extractRequestHeaders(final Representation result) {
- for (Header header : getRequestHeaders()) {
- if (header.getName().equalsIgnoreCase(HeaderConstants.HEADER_CONTENT_ENCODING)) {
- new EncodingReader(header.getValue()).addValues(result.getEncodings());
- } else if (header.getName().equalsIgnoreCase(HeaderConstants.HEADER_CONTENT_LANGUAGE)) {
- new LanguageReader(header.getValue()).addValues(result.getLanguages());
- } else if (header.getName().equalsIgnoreCase(HeaderConstants.HEADER_CONTENT_TYPE)) {
- ContentType contentType = new ContentType(header.getValue());
- result.setMediaType(contentType.getMediaType());
- result.setCharacterSet(contentType.getCharacterSet());
- } else if (header.getName().equalsIgnoreCase(HeaderConstants.HEADER_CONTENT_RANGE)) {
- RangeReader.update(header.getValue(), result);
- } else if (header.getName().equalsIgnoreCase(HeaderConstants.HEADER_CONTENT_MD5)) {
- result.setDigest(
- new Digest(
- Digest.ALGORITHM_MD5,
- Base64.getDecoder().decode(header.getValue())));
- } else if (header.getName()
- .equalsIgnoreCase(HeaderConstants.HEADER_CONTENT_DISPOSITION)) {
- try {
- result.setDisposition(new DispositionReader(header.getValue()).readValue());
- } catch (IOException ioe) {
- Context.getCurrentLogger()
- .log(
- Level.WARNING,
- ioe,
- () ->
- "Error during Content-Disposition header parsing. Header: "
- + header.getValue());
- }
- }
- }
- }
-
- /**
- * Returns the request entity stream if it exists.
- *
- * @param size The expected entity size or -1 if unknown.
- * @return The request entity stream if it exists.
- */
- public abstract InputStream getRequestEntityStream(long size);
-
- /**
- * Returns the request head stream if it exists.
- *
- * @return The request head stream if it exists.
- */
- public abstract InputStream getRequestHeadStream();
-
- /**
- * Returns the response entity stream if it exists.
- *
- * @return The response entity stream if it exists.
- */
- public abstract OutputStream getResponseEntityStream();
-
- /**
- * Returns the SSL key size, if available and accessible.
- *
- * @return The SSL key size, if available and accessible.
- */
- public Integer getSslKeySize() {
- return null;
- }
-
- /**
- * Returns the SSL session ID, in hexadecimal encoding, if available and accessible.
- *
- * @return The SSL session ID, in hexadecimal encoding, if available and accessible.
- */
- public String getSslSessionId() {
- byte[] byteArray = getSslSessionIdBytes();
-
- if (byteArray != null) {
- return IoUtils.toHexString(byteArray);
- } else {
- return null;
- }
- }
-
- /**
- * Returns the SSL session ID, as a byte array, if available and accessible in that format (to
- * be used by getSslSessionId).
- *
- * @return The SSL session ID, as a byte array, if available and accessible in that format.
- */
- protected byte[] getSslSessionIdBytes() {
- return null;
- }
-
- @Override
- protected boolean isClientKeepAlive() {
- return !HeaderUtils.isConnectionClose(getRequestHeaders());
- }
-
- @Override
- protected boolean isServerKeepAlive() {
- return true;
- }
-
- /** Parses the "host" header to set the server host and port properties. */
- private void parseHost() {
- String host = getRequestHeaders().getFirstValue(HeaderConstants.HEADER_HOST, true);
-
- if (host != null) {
- // Take care of IPV6 addresses
- int colonIndex = host.indexOf(':', host.indexOf(']'));
-
- if (colonIndex != -1) {
- super.setHostDomain(host.substring(0, colonIndex));
- super.setHostPort(Integer.parseInt(host.substring(colonIndex + 1)));
- } else {
- super.setHostDomain(host);
- super.setHostPort(getProtocol().getDefaultPort());
- }
- } else {
- getLogger().info("Couldn't find the mandatory \"Host\" HTTP header.");
- }
-
- this.hostParsed = true;
- }
-
- /**
- * Reads the HTTP request head (request line and headers).
- *
- * @throws IOException
- */
- protected void readRequestHead(InputStream headStream) throws IOException {
- StringBuilder sb = new StringBuilder();
-
- // Parse the request method
- int next = headStream.read();
- while ((next != -1) && !HeaderUtils.isSpace(next)) {
- sb.append((char) next);
- next = headStream.read();
- }
-
- if (next == -1) {
- throw new IOException(
- "Unable to parse the request method. End of stream reached too early.");
- }
-
- setMethod(sb.toString());
- sb.delete(0, sb.length());
-
- // Parse the request URI
- next = headStream.read();
- while ((next != -1) && !HeaderUtils.isSpace(next)) {
- sb.append((char) next);
- next = headStream.read();
- }
-
- if (next == -1) {
- throw new IOException(
- "Unable to parse the request URI. End of stream reached too early.");
- }
- setRequestUri(sb.toString());
- sb.delete(0, sb.length());
-
- // Parse the HTTP version
- next = headStream.read();
- while ((next != -1) && !HeaderUtils.isCarriageReturn(next)) {
- sb.append((char) next);
- next = headStream.read();
- }
-
- if (next == -1) {
- throw new IOException(
- "Unable to parse the HTTP version. End of stream reached too early.");
- }
- next = headStream.read();
-
- if (HeaderUtils.isLineFeed(next)) {
- setVersion(sb.toString());
- sb.delete(0, sb.length());
-
- // Parse the headers
- Header header = HeaderReader.readHeader(headStream, sb);
-
- while (header != null) {
- getRequestHeaders().add(header);
- header = HeaderReader.readHeader(headStream, sb);
- }
- } else {
- throw new IOException(
- "Unable to parse the HTTP version. The carriage return must be followed by a line feed.");
- }
- }
-
- /**
- * Sends the response back to the client. Commits the status, headers, and optional entity and
- * send them over the network. The default implementation only writes the response entity on the
- * response stream or channel. Subclasses will probably also copy the response headers and
- * status.
- *
- * @param response The high-level response.
- * @throws IOException if the Response could not be written to the network.
- */
- public void sendResponse(Response response) throws IOException {
- if (response != null) {
- // Get the connector service to callback
- Representation responseEntity = response.getEntity();
- ConnectorService connectorService = ConnectorHelper.getConnectorService();
-
- if (connectorService != null) {
- connectorService.beforeSend(responseEntity);
- }
-
- OutputStream responseEntityStream = null;
- try {
- writeResponseHead(response);
-
- if (responseEntity != null) {
-
- responseEntityStream = getResponseEntityStream();
- writeResponseBody(responseEntity, responseEntityStream);
- }
- } finally {
- if (responseEntityStream != null) {
- try {
- responseEntityStream.flush();
- responseEntityStream.close();
- } catch (IOException ioe) {
- // The stream was probably already closed by the
- // connector. Probably OK, low message priority.
- getLogger()
- .log(
- Level.FINE,
- "Exception while flushing and closing the entity stream.",
- ioe);
- }
- }
- if (responseEntity != null) {
- responseEntity.release();
- }
-
- if (connectorService != null) {
- connectorService.afterSend(responseEntity);
- }
- }
- }
- }
-
- /**
- * Indicates if the response should be chunked because its length is unknown.
- *
- * @param response The response to analyze.
- * @return True if the response should be chunked.
- */
- public boolean shouldResponseBeChunked(Response response) {
- return (response.getEntity() != null) && !response.getEntity().hasKnownSize();
- }
-
- /**
- * Effectively writes the response body. The entity to write is guaranteed to be non-null.
- * Attempts to write the entity on the response channel or response stream by default.
- *
- * @param entity The representation to write as entity of the body.
- * @param responseEntityStream The response entity stream or null if a channel is used.
- * @throws IOException
- */
- protected void writeResponseBody(Representation entity, OutputStream responseEntityStream)
- throws IOException {
- // Send the entity to the client
- if (responseEntityStream != null) {
- entity.write(responseEntityStream);
- responseEntityStream.flush();
- }
- }
-
- /**
- * Writes the response status line and headers. Does nothing by default.
- *
- * @param response The response.
- * @throws IOException
- */
- protected void writeResponseHead(Response response) throws IOException {
- // Do nothing by default
- }
-
- /**
- * Writes the response head to the given output stream.
- *
- * @param response The response.
- * @param headStream The output stream to write to.
- * @throws IOException
- */
- protected void writeResponseHead(Response response, OutputStream headStream)
- throws IOException {
- // Write the status line
- String version = (getVersion() == null) ? "1.1" : getVersion();
- headStream.write(StringUtils.getAsciiBytes(version));
- headStream.write(' ');
- headStream.write(StringUtils.getAsciiBytes(Integer.toString(getStatusCode())));
- headStream.write(' ');
-
- if (getReasonPhrase() != null) {
- headStream.write(StringUtils.getLatin1Bytes(getReasonPhrase()));
- } else {
- headStream.write(StringUtils.getAsciiBytes(("Status " + getStatusCode())));
- }
-
- headStream.write(13); // CR
- headStream.write(10); // LF
-
- // We don't support persistent connections yet
- getResponseHeaders().set(HeaderConstants.HEADER_CONNECTION, "close", true);
-
- // Check if 'Transfer-Encoding' header should be set
- if (shouldResponseBeChunked(response)) {
- getResponseHeaders().add(HeaderConstants.HEADER_TRANSFER_ENCODING, "chunked");
- }
-
- // Write the response headers
- for (Header header : getResponseHeaders()) {
- HeaderUtils.writeHeaderLine(header, headStream);
- }
-
- // Write the end of the headers section
- headStream.write(13); // CR
- headStream.write(10); // LF
- headStream.flush();
- }
-}
diff --git a/org.restlet/src/main/java/org/restlet/engine/adapter/package-info.java b/org.restlet/src/main/java/org/restlet/engine/adapter/package-info.java
deleted file mode 100644
index 58e1e7c2c1..0000000000
--- a/org.restlet/src/main/java/org/restlet/engine/adapter/package-info.java
+++ /dev/null
@@ -1,6 +0,0 @@
-/**
- * Adapters between low-level HTTP calls and high-level Restlet Request and Response objects.
- *
- * @since Restlet 2.0
- */
-package org.restlet.engine.adapter;
diff --git a/org.restlet/src/main/java/org/restlet/engine/netty/NettyClientCall.java b/org.restlet/src/main/java/org/restlet/engine/netty/NettyClientCall.java
index 731f05ff02..6d5bcac2d5 100644
--- a/org.restlet/src/main/java/org/restlet/engine/netty/NettyClientCall.java
+++ b/org.restlet/src/main/java/org/restlet/engine/netty/NettyClientCall.java
@@ -26,33 +26,42 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
-import java.io.OutputStream;
+import java.io.PushbackInputStream;
import java.net.ConnectException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
+import java.util.logging.Level;
import org.restlet.Uniform;
import org.restlet.data.Header;
+import org.restlet.data.Method;
+import org.restlet.data.Protocol;
import org.restlet.data.Reference;
import org.restlet.data.Status;
-import org.restlet.engine.adapter.ClientCall;
import org.restlet.engine.connector.ConnectorHelper;
import org.restlet.engine.header.HeaderConstants;
+import org.restlet.engine.header.HeaderUtils;
+import org.restlet.representation.InputRepresentation;
import org.restlet.representation.Representation;
import org.restlet.service.ConnectorService;
+import org.restlet.util.Series;
/**
- * HTTP client call based on Netty. The calling thread blocks while the request is written and until
- * the response head is received; the response body is then streamed from the connection.
+ * HTTP client exchange based on Netty, mapping a Restlet request and response to Netty messages.
+ * The calling thread blocks while the request is written and until the response head is received;
+ * the response body is then streamed from the connection.
*
* This class is internal to the Netty connectors.
*/
-class NettyClientCall extends ClientCall {
+class NettyClientCall {
/** The parent helper. */
private final NettyClientHelper helper;
+ /** The request method name. */
+ private final String method;
+
/** The request target (origin form, or absolute form for a forward proxy). */
private final String target;
@@ -62,6 +71,15 @@ class NettyClientCall extends ClientCall {
/** The target port. */
private final int targetPort;
+ /** Indicates whether the exchange is secured with TLS. */
+ private final boolean secure;
+
+ /** The request headers. */
+ private final Series requestHeaders = new Series<>(Header.class);
+
+ /** The response headers. */
+ private final Series responseHeaders = new Series<>(Header.class);
+
private final ReentrantLock lock = new ReentrantLock();
private final Condition headReceived = lock.newCondition();
@@ -75,6 +93,15 @@ class NettyClientCall extends ClientCall {
/** The response body stream. */
private volatile ChannelInputStream body;
+ /** The response status code. */
+ private volatile int statusCode;
+
+ /** The response reason phrase. */
+ private volatile String reasonPhrase;
+
+ /** The HTTP version of the response (for example "1.1" or "2.0"), or null. */
+ private volatile String responseVersion;
+
/** Indicates whether the response head was received. */
private boolean head;
@@ -108,16 +135,14 @@ class NettyClientCall extends ClientCall {
Reference resourceRef,
String target,
boolean secure) {
- super(helper, method, resourceRef.toString());
this.helper = helper;
+ this.method = method;
this.target = target;
this.targetHost = resourceRef.getHostDomain();
int port = resourceRef.getHostPort();
int defaultPort = secure ? 443 : 80;
this.targetPort = (port == -1) ? defaultPort : port;
- setConfidential(secure);
- setServerAddress(this.targetHost);
- setServerPort(this.targetPort);
+ this.secure = secure;
}
/**
@@ -137,33 +162,56 @@ int getTargetPort() {
return targetPort;
}
- @Override
- public OutputStream getRequestEntityStream() {
- return null;
- }
-
- @Override
- public OutputStream getRequestHeadStream() {
- return null;
+ boolean isConfidential() {
+ return secure;
}
- @Override
- public InputStream getResponseEntityStream(long size) {
- final ChannelInputStream stream = this.body;
- if (stream == null) {
- return null;
+ /**
+ * Sends the request and updates the response. Asynchronous requests invoke their "onSent" and
+ * "onResponse" callbacks once the response is updated.
+ *
+ * @param request The Restlet request.
+ * @param response The Restlet response to update.
+ */
+ void handle(org.restlet.Request request, org.restlet.Response response) {
+ HeaderUtils.addGeneralHeaders(request, requestHeaders);
+ if (request.getEntity() != null) {
+ HeaderUtils.addEntityHeaders(request.getEntity(), requestHeaders);
}
- return new FilterInputStream(stream) {
- @Override
- public void close() throws IOException {
- super.close();
- onBodyClosed();
+ // NOTE: This must stay at the end because the AWS challenge scheme requires access to all
+ // HTTP headers
+ HeaderUtils.addRequestHeaders(request, requestHeaders);
+
+ Status status = sendRequest(request);
+ updateResponse(response, status);
+
+ if (request.isAsynchronous()) {
+ Uniform onSent = request.getOnSent();
+ if (onSent != null) {
+ onSent.handle(request, response);
}
- };
+ Uniform onResponse = request.getOnResponse();
+ if (onResponse != null) {
+ try {
+ onResponse.handle(request, response);
+ } catch (RuntimeException e) {
+ helper.getLogger()
+ .log(
+ Level.WARNING,
+ "Unexpected error or exception inside the user call back",
+ e);
+ }
+ }
+ }
}
- @Override
- public Status sendRequest(org.restlet.Request request) {
+ /**
+ * Sends the request and waits for the response head.
+ *
+ * @param request The Restlet request.
+ * @return The response status, or a connector error status.
+ */
+ private Status sendRequest(org.restlet.Request request) {
Status result;
Representation entity = request.isEntityAvailable() ? request.getEntity() : null;
ConnectorService connectorService = ConnectorHelper.getConnectorService();
@@ -175,7 +223,7 @@ public Status sendRequest(org.restlet.Request request) {
this.channel = helper.acquire(this);
writeRequest(entity);
awaitHead();
- result = new Status(getStatusCode(), getReasonPhrase());
+ result = new Status(statusCode, reasonPhrase);
} catch (ConnectException e) {
helper.logFailure("Unable to connect to the remote HTTP server", e);
abort();
@@ -206,19 +254,109 @@ public Status sendRequest(org.restlet.Request request) {
return result;
}
- @Override
- public void sendRequest(
- org.restlet.Request request, org.restlet.Response response, Uniform callback) {
- sendRequest(request);
+ /**
+ * Updates the Restlet response with the status, headers and entity received.
+ *
+ * @param response The Restlet response.
+ * @param status The response status.
+ */
+ private void updateResponse(org.restlet.Response response, Status status) {
+ response.setStatus(status);
+ response.getServerInfo().setAddress(targetHost);
+ response.getServerInfo().setPort(targetPort);
+ if (responseVersion != null) {
+ response.setProtocol(Protocol.valueOf(secure ? "https" : "http", responseVersion));
+ }
+ response.getAttributes().put(HeaderConstants.ATTRIBUTE_HEADERS, responseHeaders);
+
+ try {
+ HeaderUtils.copyResponseTransportHeaders(responseHeaders, response);
+ } catch (RuntimeException e) {
+ helper.getLogger()
+ .log(Level.FINE, "Unable to process the headers of the HTTP response.", e);
+ response.setStatus(Status.CONNECTOR_ERROR_INTERNAL, e);
+ }
+
+ Representation entity = getResponseEntity(response.getStatus());
+ response.setEntity(entity);
+
+ if (entity != null && (entity.isEmpty() || !hasBody(response.getStatus()))) {
+ entity.release();
+ if (Status.SUCCESS_RESET_CONTENT.equals(response.getStatus())
+ || response.getStatus().isInformational()) {
+ response.setEntity(null);
+ }
+ }
+ }
+
+ /** Indicates whether a response with the given status to this request has a body. */
+ private boolean hasBody(Status status) {
+ return !Method.HEAD.getName().equals(method)
+ && !status.isInformational()
+ && !Status.REDIRECTION_NOT_MODIFIED.equals(status)
+ && !Status.SUCCESS_NO_CONTENT.equals(status)
+ && !Status.SUCCESS_RESET_CONTENT.equals(status);
+ }
+
+ /**
+ * Returns the response entity, with the metadata of the entity headers, or null.
+ *
+ * @param status The response status.
+ * @return The response entity, or null.
+ */
+ private Representation getResponseEntity(Status status) {
+ Representation result = null;
+ String transferEncoding =
+ responseHeaders.getFirstValue(HeaderConstants.HEADER_TRANSFER_ENCODING, true);
+ long size =
+ (transferEncoding != null && !"identity".equalsIgnoreCase(transferEncoding))
+ ? Representation.UNKNOWN_SIZE
+ : HeaderUtils.getContentLength(responseHeaders);
+
+ if (hasBody(status)) {
+ InputStream stream = nonEmpty(getResponseEntityStream());
+ if (stream != null) {
+ result = new InputRepresentation(stream, null);
+ result.setSize(size);
+ }
+ }
+
+ return HeaderUtils.extractEntityHeaders(responseHeaders, result);
+ }
- final Uniform onSent = request.getOnSent();
- if (onSent != null) {
- onSent.handle(request, response);
+ /** Returns a stream equivalent to the given one, or null if it is null or empty. */
+ private static InputStream nonEmpty(InputStream stream) {
+ if (stream == null) {
+ return null;
}
+ try {
+ if (stream.available() > 0) {
+ return stream;
+ }
+ PushbackInputStream result = new PushbackInputStream(stream);
+ int next = result.read();
+ if (next >= 0) {
+ result.unread(next);
+ return result;
+ }
+ } catch (IOException _) {
+ // End of the response entity stream.
+ }
+ return null;
+ }
- if (callback != null) {
- callback.handle(request, response);
+ private InputStream getResponseEntityStream() {
+ final ChannelInputStream stream = this.body;
+ if (stream == null) {
+ return null;
}
+ return new FilterInputStream(stream) {
+ @Override
+ public void close() throws IOException {
+ super.close();
+ onBodyClosed();
+ }
+ };
}
private void writeRequest(Representation entity) throws IOException {
@@ -259,11 +397,10 @@ private void writeRequest(Representation entity) throws IOException {
*/
private HttpRequest newRequestHead() {
HttpRequest result =
- new DefaultHttpRequest(
- HttpVersion.HTTP_1_1, HttpMethod.valueOf(getMethod()), target);
+ new DefaultHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method), target);
HttpHeaders headers = result.headers();
- for (Header header : getRequestHeaders()) {
+ for (Header header : requestHeaders) {
String name = header.getName();
if (!HeaderConstants.HEADER_CONTENT_LENGTH.equalsIgnoreCase(name)
&& !HeaderConstants.HEADER_TRANSFER_ENCODING.equalsIgnoreCase(name)) {
@@ -283,7 +420,7 @@ private HttpRequest newRequestHead() {
// header.
headers.set(
(kind == StreamKind.HTTP3) ? "x-http3-scheme" : "x-http2-scheme",
- isConfidential() ? "https" : "http");
+ secure ? "https" : "http");
} else if (!headers.contains(HttpHeaderNames.CONNECTION)) {
headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
@@ -299,7 +436,7 @@ private HttpRequest newRequestHead() {
*/
private boolean setBodyHeaders(HttpRequest request, Representation entity) {
if (entity == null) {
- if (requiresContentLength(getMethod())) {
+ if (requiresContentLength(method)) {
HttpUtil.setContentLength(request, 0);
}
return false;
@@ -365,7 +502,7 @@ void onResponseHead(HttpResponse response) {
String name = entry.getKey();
if (!name.regionMatches(true, 0, "x-http2-", 0, 8)
&& !name.regionMatches(true, 0, "x-http3-", 0, 8)) {
- getResponseHeaders().add(name, entry.getValue());
+ responseHeaders.add(name, entry.getValue());
}
}
if (!HttpUtil.isKeepAlive(response)) {
@@ -377,9 +514,15 @@ void onResponseHead(HttpResponse response) {
}
}
- setStatusCode(response.status().code());
- setReasonPhrase(response.status().reasonPhrase());
- setVersion(response.protocolVersion().text());
+ this.statusCode = response.status().code();
+ this.reasonPhrase = response.status().reasonPhrase();
+ // The HTTP/2 and HTTP/3 codecs convert stream frames to HTTP/1.1 objects.
+ this.responseVersion =
+ kind.isStream()
+ ? kind.version().substring(kind.version().indexOf('/') + 1)
+ : response.protocolVersion().majorVersion()
+ + "."
+ + response.protocolVersion().minorVersion();
this.body = new ChannelInputStream(channel, helper.getIdleTimeout());
lock.lock();
diff --git a/org.restlet/src/main/java/org/restlet/engine/netty/NettyClientHelper.java b/org.restlet/src/main/java/org/restlet/engine/netty/NettyClientHelper.java
index 0a17db61a5..2f14deefca 100644
--- a/org.restlet/src/main/java/org/restlet/engine/netty/NettyClientHelper.java
+++ b/org.restlet/src/main/java/org/restlet/engine/netty/NettyClientHelper.java
@@ -66,8 +66,7 @@
import org.restlet.data.Method;
import org.restlet.data.Reference;
import org.restlet.data.Status;
-import org.restlet.engine.adapter.ClientCall;
-import org.restlet.engine.adapter.HttpClientHelper;
+import org.restlet.engine.connector.ClientHelper;
import org.restlet.engine.ssl.SslUtils;
import org.restlet.engine.util.ReferenceUtils;
import org.restlet.representation.Representation;
@@ -197,7 +196,7 @@
*
* @author Jerome Louvel
*/
-public abstract class NettyClientHelper extends HttpClientHelper {
+public abstract class NettyClientHelper extends ClientHelper {
/** The negotiated application protocol of a new connection. */
private static final AttributeKey> READY =
@@ -271,8 +270,13 @@ protected NettyClientHelper(Client client) {
super(client);
}
- @Override
- public ClientCall create(Request request) {
+ /**
+ * Creates the exchange of a request.
+ *
+ * @param request The Restlet request.
+ * @return The exchange.
+ */
+ NettyClientCall create(Request request) {
Reference ref = ReferenceUtils.update(request.getResourceRef(), request);
String scheme = ref.getSchemeProtocol() == null ? null : ref.getScheme();
@@ -289,7 +293,7 @@ public ClientCall create(Request request) {
@Override
public void handle(Request request, Response response) {
if (!isFollowRedirects() || request.isAsynchronous()) {
- super.handle(request, response);
+ handleOnce(request, response);
return;
}
@@ -300,7 +304,7 @@ public void handle(Request request, Response response) {
Request next = current;
while (next != null) {
current = next;
- super.handle(current, response);
+ handleOnce(current, response);
next = (redirects < getMaxRedirects()) ? nextRedirect(current, response) : null;
if (next != null) {
if (response.getEntity() != null) {
@@ -320,6 +324,21 @@ public void handle(Request request, Response response) {
response.setRequest(request);
}
+ /**
+ * Handles one exchange, without following redirects.
+ *
+ * @param request The Restlet request.
+ * @param response The Restlet response.
+ */
+ private void handleOnce(Request request, Response response) {
+ try {
+ create(request).handle(request, response);
+ } catch (RuntimeException e) {
+ getLogger().log(Level.INFO, "Error while handling an HTTP client call", e);
+ response.setStatus(Status.CONNECTOR_ERROR_INTERNAL, e);
+ }
+ }
+
/**
* Returns the request following a redirect response, or null if the response is not a redirect
* that can be followed.
diff --git a/org.restlet/src/main/java/org/restlet/engine/netty/NettyRequest.java b/org.restlet/src/main/java/org/restlet/engine/netty/NettyRequest.java
new file mode 100644
index 0000000000..d8db5ef6a5
--- /dev/null
+++ b/org.restlet/src/main/java/org/restlet/engine/netty/NettyRequest.java
@@ -0,0 +1,457 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.netty;
+
+import static org.restlet.engine.header.HeaderConstants.HEADER_IF_MATCH;
+import static org.restlet.engine.header.HeaderConstants.HEADER_IF_MODIFIED_SINCE;
+import static org.restlet.engine.header.HeaderConstants.HEADER_IF_NONE_MATCH;
+import static org.restlet.engine.header.HeaderConstants.HEADER_IF_RANGE;
+import static org.restlet.engine.header.HeaderConstants.HEADER_IF_UNMODIFIED_SINCE;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.data.CacheDirective;
+import org.restlet.data.ChallengeResponse;
+import org.restlet.data.ClientInfo;
+import org.restlet.data.Conditions;
+import org.restlet.data.Cookie;
+import org.restlet.data.Header;
+import org.restlet.data.Method;
+import org.restlet.data.Protocol;
+import org.restlet.data.Range;
+import org.restlet.data.RecipientInfo;
+import org.restlet.data.Reference;
+import org.restlet.data.Tag;
+import org.restlet.data.Warning;
+import org.restlet.engine.header.CacheDirectiveReader;
+import org.restlet.engine.header.CookieReader;
+import org.restlet.engine.header.ExpectationReader;
+import org.restlet.engine.header.HeaderConstants;
+import org.restlet.engine.header.HeaderReader;
+import org.restlet.engine.header.PreferenceReader;
+import org.restlet.engine.header.RangeReader;
+import org.restlet.engine.header.RecipientInfoReader;
+import org.restlet.engine.header.StringReader;
+import org.restlet.engine.header.TagReader;
+import org.restlet.engine.header.WarningReader;
+import org.restlet.engine.security.AuthenticatorUtils;
+import org.restlet.engine.util.DateUtils;
+import org.restlet.engine.util.ReferenceUtils;
+import org.restlet.engine.util.StringUtils;
+import org.restlet.representation.Representation;
+import org.restlet.util.Series;
+
+/**
+ * Restlet request of a {@link NettyServerCall}. The request headers are parsed lazily, when the
+ * corresponding property is first read.
+ *
+ * @author Jerome Louvel
+ */
+class NettyRequest extends Request {
+
+ /** The exchange. */
+ private final NettyServerCall call;
+
+ /** The request headers. */
+ private final Series headers;
+
+ /** The context of the server connector. */
+ private final Context context;
+
+ private volatile boolean accessControlRequestHeadersAdded;
+
+ private volatile boolean accessControlRequestMethodAdded;
+
+ private volatile boolean cacheDirectivesAdded;
+
+ private volatile boolean clientAdded;
+
+ private volatile boolean conditionAdded;
+
+ private volatile boolean cookiesAdded;
+
+ private volatile boolean entityAdded;
+
+ private volatile boolean proxySecurityAdded;
+
+ private volatile boolean rangesAdded;
+
+ private volatile boolean recipientsInfoAdded;
+
+ private volatile boolean referrerAdded;
+
+ private volatile boolean securityAdded;
+
+ private volatile boolean warningsAdded;
+
+ /**
+ * Constructor.
+ *
+ * @param context The context of the server connector.
+ * @param call The exchange.
+ */
+ NettyRequest(Context context, NettyServerCall call) {
+ this.context = context;
+ this.call = call;
+ this.headers = call.getRequestHeaders();
+
+ setMethod(Method.valueOf(call.getMethod()));
+ setHostRef(hostRef(call.getProtocol(), headers));
+
+ String uri = call.getRequestUri();
+ Reference resourceRef = new Reference(getHostRef(), uri);
+ if (resourceRef.isRelative()) {
+ // Take care of the "/" between the host part and the segments.
+ resourceRef =
+ new Reference(getHostRef().toString() + (uri.startsWith("/") ? "" : "/") + uri);
+ }
+ setResourceRef(resourceRef);
+ setOriginalRef(ReferenceUtils.getOriginalRef(getResourceRef(), headers));
+
+ String dateHeader = headers.getFirstValue(HeaderConstants.HEADER_DATE, true);
+ Date date = (dateHeader != null) ? DateUtils.parse(dateHeader) : null;
+ setDate((date != null) ? date : new Date());
+ }
+
+ /** Returns the host reference built from the "Host" header. */
+ private static String hostRef(Protocol protocol, Series headers) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(protocol.getSchemeName()).append("://");
+ String host = headers.getFirstValue(HeaderConstants.HEADER_HOST, true);
+
+ if (host == null) {
+ Context.getCurrentLogger().info("Couldn't find the mandatory \"Host\" HTTP header.");
+ sb.append("null");
+ return sb.toString();
+ }
+
+ // Take care of IPv6 addresses
+ int colonIndex = host.indexOf(':', host.indexOf(']'));
+ if (colonIndex == -1) {
+ sb.append(host);
+ } else {
+ sb.append(host, 0, colonIndex);
+ String port = host.substring(colonIndex + 1);
+ if (!port.equals(Integer.toString(protocol.getDefaultPort()))) {
+ sb.append(':').append(port);
+ }
+ }
+ return sb.toString();
+ }
+
+ private Logger getLogger() {
+ return (context != null) ? context.getLogger() : Context.getCurrentLogger();
+ }
+
+ @Override
+ public boolean abort() {
+ return call.abort();
+ }
+
+ @Override
+ public void flushBuffers() throws IOException {
+ call.flushBuffers();
+ }
+
+ @Override
+ public Set getAccessControlRequestHeaders() {
+ Set result = super.getAccessControlRequestHeaders();
+ if (!accessControlRequestHeadersAdded) {
+ for (String header :
+ headers.getValuesArray(
+ HeaderConstants.HEADER_ACCESS_CONTROL_REQUEST_HEADERS, true)) {
+ new StringReader(header).addValues(result);
+ }
+ accessControlRequestHeadersAdded = true;
+ }
+ return result;
+ }
+
+ @Override
+ public Method getAccessControlRequestMethod() {
+ Method result = super.getAccessControlRequestMethod();
+ if (!accessControlRequestMethodAdded) {
+ String header =
+ headers.getFirstValue(
+ HeaderConstants.HEADER_ACCESS_CONTROL_REQUEST_METHOD, true);
+ if (header != null) {
+ result = Method.valueOf(header);
+ super.setAccessControlRequestMethod(result);
+ }
+ accessControlRequestMethodAdded = true;
+ }
+ return result;
+ }
+
+ @Override
+ public List getCacheDirectives() {
+ List result = super.getCacheDirectives();
+ if (!cacheDirectivesAdded) {
+ for (Header header : headers.subList(HeaderConstants.HEADER_CACHE_CONTROL)) {
+ CacheDirectiveReader.addValues(header, result);
+ }
+ cacheDirectivesAdded = true;
+ }
+ return result;
+ }
+
+ @Override
+ public ChallengeResponse getChallengeResponse() {
+ ChallengeResponse result = super.getChallengeResponse();
+ if (!securityAdded) {
+ String authorization = headers.getValues(HeaderConstants.HEADER_AUTHORIZATION);
+ result = AuthenticatorUtils.parseResponse(this, authorization, headers);
+ setChallengeResponse(result);
+ }
+ return result;
+ }
+
+ @Override
+ public ClientInfo getClientInfo() {
+ ClientInfo result = super.getClientInfo();
+ if (clientAdded) {
+ return result;
+ }
+ clientAdded = true;
+
+ addPreferences(result);
+ result.setAgent(headers.getValues(HeaderConstants.HEADER_USER_AGENT));
+ result.setFrom(headers.getFirstValue(HeaderConstants.HEADER_FROM, true));
+
+ InetSocketAddress remote = call.getRemoteAddress();
+ if (remote != null) {
+ result.setAddress(remote.getAddress().getHostAddress());
+ result.setPort(remote.getPort());
+ }
+
+ // Special handling for the non-standard but common "X-Forwarded-For" header, supported by
+ // popular proxies and caches.
+ if (context != null
+ && Boolean.parseBoolean(
+ context.getParameters().getFirstValue("useForwardedForHeader", false))) {
+ String header = headers.getValues(HeaderConstants.HEADER_X_FORWARDED_FOR);
+ if (header != null) {
+ for (String address : header.split(",")) {
+ result.getForwardedAddresses().add(address.trim());
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Parses the Accept* and Expect headers. A parsing error is traced, and the other headers are
+ * still parsed.
+ */
+ private void addPreferences(ClientInfo result) {
+ addPreference(
+ HeaderConstants.HEADER_ACCEPT_CHARSET,
+ v -> PreferenceReader.addCharacterSets(v, result));
+ addPreference(
+ HeaderConstants.HEADER_ACCEPT_ENCODING,
+ v -> PreferenceReader.addEncodings(v, result));
+ addPreference(
+ HeaderConstants.HEADER_ACCEPT_LANGUAGE,
+ v -> PreferenceReader.addLanguages(v, result));
+ addPreference(
+ HeaderConstants.HEADER_ACCEPT, v -> PreferenceReader.addMediaTypes(v, result));
+ addPreference(
+ HeaderConstants.HEADER_ACCEPT_PATCH, v -> PreferenceReader.addPatches(v, result));
+ addPreference(HeaderConstants.HEADER_EXPECT, v -> ExpectationReader.addValues(v, result));
+ }
+
+ private void addPreference(String name, java.util.function.Consumer parser) {
+ try {
+ parser.accept(headers.getValues(name));
+ } catch (RuntimeException e) {
+ getLogger().log(Level.INFO, e.getMessage());
+ }
+ }
+
+ @Override
+ public Conditions getConditions() {
+ Conditions result = super.getConditions();
+ if (conditionAdded) {
+ return result;
+ }
+ conditionAdded = true;
+
+ for (Header header : headers) {
+ if (header.getName().equalsIgnoreCase(HEADER_IF_MODIFIED_SINCE)) {
+ Date date = HeaderReader.readDate(header.getValue(), false);
+ if (date != null && date.getTime() != -1) {
+ result.setModifiedSince(date);
+ }
+ } else if (header.getName().equalsIgnoreCase(HEADER_IF_UNMODIFIED_SINCE)) {
+ Date date = HeaderReader.readDate(header.getValue(), false);
+ if (date != null && date.getTime() != -1) {
+ result.setUnmodifiedSince(date);
+ }
+ }
+ }
+
+ addTags(HEADER_IF_MATCH, result.getMatch());
+ addTags(HEADER_IF_NONE_MATCH, result.getNoneMatch());
+
+ String ifRange = headers.getFirstValue(HEADER_IF_RANGE, true);
+ if (!StringUtils.isNullOrEmpty(ifRange)) {
+ Tag tag = Tag.parse(ifRange);
+ if (tag != null) {
+ result.setRangeTag(tag);
+ } else {
+ result.setRangeDate(HeaderReader.readDate(ifRange, false));
+ }
+ }
+
+ return result;
+ }
+
+ private void addTags(String headerName, List tags) {
+ String value = headers.getValues(headerName);
+ if (value != null) {
+ try {
+ new TagReader(value).addValues(tags);
+ } catch (RuntimeException _) {
+ getLogger()
+ .log(
+ Level.INFO,
+ "Unable to process the {0} header: {1}",
+ new Object[] {headerName, value});
+ }
+ }
+ }
+
+ @Override
+ public Series getCookies() {
+ Series result = super.getCookies();
+ if (!cookiesAdded) {
+ String cookies = headers.getValues(HeaderConstants.HEADER_COOKIE);
+ if (cookies != null) {
+ new CookieReader(cookies).addValues(result);
+ }
+ cookiesAdded = true;
+ }
+ return result;
+ }
+
+ @Override
+ public Representation getEntity() {
+ if (!entityAdded) {
+ setEntity(call.getRequestEntity());
+ }
+ return super.getEntity();
+ }
+
+ @Override
+ public ChallengeResponse getProxyChallengeResponse() {
+ ChallengeResponse result = super.getProxyChallengeResponse();
+ if (!proxySecurityAdded) {
+ String authorization = headers.getValues(HeaderConstants.HEADER_PROXY_AUTHORIZATION);
+ result = AuthenticatorUtils.parseResponse(this, authorization, headers);
+ setProxyChallengeResponse(result);
+ }
+ return result;
+ }
+
+ @Override
+ public List getRanges() {
+ List result = super.getRanges();
+ if (!rangesAdded) {
+ result.addAll(RangeReader.read(headers.getValues(HeaderConstants.HEADER_RANGE)));
+ rangesAdded = true;
+ }
+ return result;
+ }
+
+ @Override
+ public List getRecipientsInfo() {
+ List result = super.getRecipientsInfo();
+ if (!recipientsInfoAdded) {
+ for (String header : headers.getValuesArray(HeaderConstants.HEADER_VIA, true)) {
+ new RecipientInfoReader(header).addValues(result);
+ }
+ recipientsInfoAdded = true;
+ }
+ return result;
+ }
+
+ @Override
+ public Reference getReferrerRef() {
+ if (!referrerAdded) {
+ String referrer = headers.getValues(HeaderConstants.HEADER_REFERRER);
+ if (referrer != null) {
+ setReferrerRef(new Reference(referrer));
+ }
+ referrerAdded = true;
+ }
+ return super.getReferrerRef();
+ }
+
+ @Override
+ public List getWarnings() {
+ List result = super.getWarnings();
+ if (!warningsAdded) {
+ for (String header : headers.getValuesArray(HeaderConstants.HEADER_WARNING, true)) {
+ new WarningReader(header).addValues(result);
+ }
+ warningsAdded = true;
+ }
+ return result;
+ }
+
+ @Override
+ public void setAccessControlRequestHeaders(Set accessControlRequestHeaders) {
+ super.setAccessControlRequestHeaders(accessControlRequestHeaders);
+ accessControlRequestHeadersAdded = true;
+ }
+
+ @Override
+ public void setAccessControlRequestMethod(Method accessControlRequestMethod) {
+ super.setAccessControlRequestMethod(accessControlRequestMethod);
+ accessControlRequestMethodAdded = true;
+ }
+
+ @Override
+ public void setChallengeResponse(ChallengeResponse response) {
+ super.setChallengeResponse(response);
+ securityAdded = true;
+ }
+
+ @Override
+ public void setEntity(Representation entity) {
+ super.setEntity(entity);
+ entityAdded = true;
+ }
+
+ @Override
+ public void setProxyChallengeResponse(ChallengeResponse response) {
+ super.setProxyChallengeResponse(response);
+ proxySecurityAdded = true;
+ }
+
+ @Override
+ public void setRecipientsInfo(List recipientsInfo) {
+ super.setRecipientsInfo(recipientsInfo);
+ recipientsInfoAdded = true;
+ }
+
+ @Override
+ public void setWarnings(List warnings) {
+ super.setWarnings(warnings);
+ warningsAdded = true;
+ }
+}
diff --git a/org.restlet/src/main/java/org/restlet/engine/netty/NettyServerCall.java b/org.restlet/src/main/java/org/restlet/engine/netty/NettyServerCall.java
index e631df936d..29fbdb5311 100644
--- a/org.restlet/src/main/java/org/restlet/engine/netty/NettyServerCall.java
+++ b/org.restlet/src/main/java/org/restlet/engine/netty/NettyServerCall.java
@@ -8,6 +8,11 @@
*/
package org.restlet.engine.netty;
+import static org.restlet.engine.header.HeaderConstants.ATTRIBUTE_HEADERS;
+import static org.restlet.engine.header.HeaderConstants.ATTRIBUTE_HTTPS_KEY_SIZE;
+import static org.restlet.engine.header.HeaderConstants.ATTRIBUTE_HTTPS_SSL_SESSION_ID;
+import static org.restlet.engine.header.HeaderConstants.ATTRIBUTE_VERSION;
+
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
@@ -25,6 +30,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.io.PushbackInputStream;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
@@ -33,22 +39,39 @@
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
+import org.restlet.Context;
+import org.restlet.Response;
import org.restlet.data.Header;
-import org.restlet.engine.adapter.ServerCall;
+import org.restlet.data.Method;
+import org.restlet.data.Protocol;
+import org.restlet.data.Status;
+import org.restlet.engine.Engine;
+import org.restlet.engine.connector.ConnectorHelper;
import org.restlet.engine.header.HeaderConstants;
+import org.restlet.engine.header.HeaderUtils;
import org.restlet.engine.ssl.SslUtils;
+import org.restlet.representation.EmptyRepresentation;
+import org.restlet.representation.InputRepresentation;
+import org.restlet.representation.Representation;
+import org.restlet.service.ConnectorService;
import org.restlet.util.Series;
/**
- * Server call bridging a Netty HTTP exchange (HTTP/1.1, or one HTTP/2 stream converted to HTTP/1
- * objects) to the Restlet engine. The request body is exposed as a blocking stream fed by the event
- * loop; the response body is written through a blocking stream with back-pressure.
+ * Server exchange bridging a Netty HTTP request (HTTP/1.1, or one HTTP/2 or HTTP/3 stream converted
+ * to HTTP/1 objects) to a Restlet {@link org.restlet.Request} and {@link Response}. The request
+ * body is exposed as a blocking stream fed by the event loop; the response body is written through
+ * a blocking stream with back-pressure.
*
* @author Jerome Louvel
*/
-class NettyServerCall extends ServerCall {
+class NettyServerCall {
+
+ /** The parent helper. */
+ private final NettyServerHelper helper;
/** The channel carrying this exchange (a child stream channel for HTTP/2 and HTTP/3). */
private final Channel channel;
@@ -59,23 +82,26 @@ class NettyServerCall extends ServerCall {
/** The kind of channel carrying the exchange. */
private final StreamKind kind;
- /** Indicates whether the exchange is carried by a stream of a multiplexed connection. */
- private final boolean http2;
-
/** The Netty request head. */
private final HttpRequest request;
/** The request body stream fed by the event loop. */
private final ChannelInputStream requestBody;
- /** The response output buffer size. */
- private final int outputBufferSize;
+ /** Indicates whether the connection is secured with TLS. */
+ private final boolean confidential;
+
+ /** The request headers, copied once from the Netty request head. */
+ private final Series requestHeaders;
- /** The write timeout in milliseconds. */
- private final long writeTimeoutMs;
+ /** The response headers. */
+ private final Series responseHeaders = new Series<>(Header.class);
- /** Indicates whether the request headers were copied. */
- private volatile boolean requestHeadersAdded;
+ /** The response status code. */
+ private volatile int statusCode = 200;
+
+ /** The response reason phrase. */
+ private volatile String reasonPhrase;
/** Indicates whether the response head was written. */
private volatile boolean responseHeadWritten;
@@ -106,47 +132,34 @@ class NettyServerCall extends ServerCall {
HttpRequest request,
ChannelInputStream requestBody,
boolean confidential) {
- super(helper.getHelped());
+ this.helper = helper;
this.channel = channel;
this.kind = kind;
- this.http2 = kind.isStream();
- this.connection = this.http2 ? channel.parent() : channel;
+ this.connection = kind.isStream() ? channel.parent() : channel;
this.request = request;
this.requestBody = requestBody;
- this.outputBufferSize = helper.getHttpOutputBufferSize();
- this.writeTimeoutMs = helper.getConnectorIdleTimeout();
- setConfidential(confidential);
- setMethod(request.method().name());
- setRequestUri(request.uri());
- setVersion(kind.version());
-
- InetSocketAddress remote = toInet(remoteAddress());
- if (remote != null) {
- setClientAddress(remote.getAddress().getHostAddress());
- setClientPort(remote.getPort());
- }
- InetSocketAddress local = toInet(localAddress());
- if (local != null) {
- setServerAddress(local.getAddress().getHostAddress());
- setServerPort(local.getPort());
- }
- }
-
- /** The UDP address of a QUIC connection, or the TCP address of other connections. */
- private SocketAddress remoteAddress() {
- return (kind == StreamKind.HTTP3)
- ? Http3Support.remoteSocketAddress(connection)
- : connection.remoteAddress();
+ this.confidential = confidential;
+ this.requestHeaders = copyHeaders(request);
}
- private SocketAddress localAddress() {
- return (kind == StreamKind.HTTP3)
- ? Http3Support.localSocketAddress(connection)
- : connection.localAddress();
- }
-
- private static InetSocketAddress toInet(SocketAddress address) {
- return (address instanceof InetSocketAddress inet) ? inet : null;
+ /**
+ * Copies the Netty request headers, except the HTTP/2 pseudo-headers. HTTP/2 and HTTP/3 carry
+ * the host in the ":authority" pseudo-header, mapped to a "Host" header when absent.
+ */
+ private static Series copyHeaders(HttpRequest request) {
+ Series result = new Series<>(Header.class);
+ for (Map.Entry header : request.headers()) {
+ if (!header.getKey().startsWith(":")) {
+ result.add(header.getKey(), header.getValue());
+ }
+ }
+ if (result.getFirstValue(HeaderConstants.HEADER_HOST, true) == null) {
+ String authority = request.headers().get(":authority");
+ if (authority != null) {
+ result.add(HeaderConstants.HEADER_HOST, authority);
+ }
+ }
+ return result;
}
/**
@@ -159,155 +172,326 @@ void setAltSvc(String altSvc) {
this.altSvc = altSvc;
}
- @Override
- public boolean abort() {
- connection.close();
- return true;
+ private Logger getLogger() {
+ return helper.getLogger();
}
- @Override
- public void complete() {
- if (completed) {
- return;
- }
- completed = true;
+ // ------------------------------------------------------------------ request side
- try {
- if (!responseHeadWritten) {
- writeHead(true);
+ /**
+ * Creates the Restlet request of this exchange.
+ *
+ * @param context The context of the server connector.
+ * @return The Restlet request.
+ */
+ NettyRequest toRequest(Context context) {
+ NettyRequest result = new NettyRequest(context, this);
+ result.getAttributes().put(ATTRIBUTE_HEADERS, requestHeaders);
+ result.getAttributes().put(ATTRIBUTE_VERSION, kind.version());
+
+ if (confidential) {
+ List certificates = getCertificates();
+ if (!certificates.isEmpty()) {
+ result.getClientInfo().setCertificates(certificates);
}
- if (responseBody != null) {
- responseBody.close();
+
+ String cipherSuite = getCipherSuite();
+ if (cipherSuite != null) {
+ result.getClientInfo().setCipherSuite(cipherSuite);
+ Integer keySize = SslUtils.extractKeySize(cipherSuite);
+ if (keySize != null) {
+ result.getAttributes().put(ATTRIBUTE_HTTPS_KEY_SIZE, keySize);
+ }
}
- } catch (IOException e) {
- getLogger().fine("Unable to complete the response: " + e.getMessage());
- }
- if (!channel.isActive()) {
- requestBody.close();
- return;
+ String sslSessionId = getSslSessionId();
+ if (sslSessionId != null) {
+ result.getAttributes().put(ATTRIBUTE_HTTPS_SSL_SESSION_ID, sslSessionId);
+ }
}
- boolean keepAlive = HttpUtil.isKeepAlive(request) && requestBody.isFullyConsumed();
- ChannelFuture future = channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
- requestBody.close();
+ return result;
+ }
- if (!http2 && !keepAlive) {
- future.addListener(ChannelFutureListener.CLOSE);
- } else if (!http2) {
- // Ready for the next request on this connection.
- future.addListener(f -> channel.read());
+ /**
+ * Creates the Restlet response of this exchange.
+ *
+ * @param request The Restlet request.
+ * @return The Restlet response.
+ */
+ Response toResponse(NettyRequest request) {
+ Response result = new Response(request);
+ InetSocketAddress local = getLocalAddress();
+ if (local != null) {
+ result.getServerInfo().setAddress(local.getAddress().getHostAddress());
+ result.getServerInfo().setPort(local.getPort());
}
+ result.getServerInfo().setAgent(Engine.VERSION_HEADER);
+ return result;
}
- @Override
- public void flushBuffers() throws IOException {
- if (responseBody != null) {
- responseBody.flush();
- } else {
- channel.flush();
- }
+ /** The request method name. */
+ String getMethod() {
+ return request.method().name();
}
- @Override
- public List getCertificates() {
- SSLSession session = sslSession();
- if (session != null) {
- try {
- return Arrays.asList(session.getPeerCertificates());
- } catch (SSLPeerUnverifiedException _) {
- return List.of();
- }
- }
- return List.of();
+ /** The request target, "/" when empty. */
+ String getRequestUri() {
+ String uri = request.uri();
+ return (uri == null || uri.isEmpty()) ? "/" : uri;
}
- @Override
- public String getCipherSuite() {
- SSLSession session = sslSession();
- return (session != null) ? session.getCipherSuite() : null;
+ /** The request headers. */
+ Series getRequestHeaders() {
+ return requestHeaders;
}
- @Override
- public InputStream getRequestEntityStream(long size) {
- return requestBody;
+ /** The exact protocol (HTTP or HTTPS). */
+ Protocol getProtocol() {
+ return confidential ? Protocol.HTTPS : Protocol.HTTP;
}
- @Override
- public Series getRequestHeaders() {
- final Series result = super.getRequestHeaders();
+ /** The remote address, or null. */
+ InetSocketAddress getRemoteAddress() {
+ SocketAddress address =
+ (kind == StreamKind.HTTP3)
+ ? Http3Support.remoteSocketAddress(connection)
+ : connection.remoteAddress();
+ return (address instanceof InetSocketAddress inet) ? inet : null;
+ }
- if (!this.requestHeadersAdded) {
- for (Map.Entry header : request.headers()) {
- String name = header.getKey();
- // HTTP/2 pseudo-headers are not HTTP headers.
- if (!name.startsWith(":")) {
- result.add(name, header.getValue());
- }
- }
- // HTTP/2 carries the host in the :authority pseudo-header, which the codec maps to a
- // regular "host" header only when absent; make sure Restlet sees a Host header.
- if (result.getFirstValue(HeaderConstants.HEADER_HOST, true) == null) {
- String authority = request.headers().get(":authority");
- if (authority != null) {
- result.add(HeaderConstants.HEADER_HOST, authority);
- }
+ /** The local address, or null. */
+ InetSocketAddress getLocalAddress() {
+ SocketAddress address =
+ (kind == StreamKind.HTTP3)
+ ? Http3Support.localSocketAddress(connection)
+ : connection.localAddress();
+ return (address instanceof InetSocketAddress inet) ? inet : null;
+ }
+
+ /**
+ * Returns the request entity, if the request declares a length, a chunked transfer coding, or a
+ * connection close (in which case the stream is peeked to detect a body).
+ *
+ * @return The request entity.
+ */
+ Representation getRequestEntity() {
+ long contentLength = HeaderUtils.getContentLength(requestHeaders);
+ boolean chunked = HeaderUtils.isChunkedEncoding(requestHeaders);
+ boolean connectionClosed = HeaderUtils.isConnectionClose(requestHeaders);
+ Representation result = null;
+
+ if ((contentLength != Representation.UNKNOWN_SIZE && contentLength != 0) || chunked) {
+ result = new InputRepresentation(requestBody, null, contentLength);
+ } else if (connectionClosed) {
+ InputStream stream = peek(requestBody);
+ if (stream != null) {
+ result = new InputRepresentation(stream, null, contentLength);
}
- this.requestHeadersAdded = true;
}
- return result;
+ if (result == null) {
+ result = new EmptyRepresentation();
+ }
+ return HeaderUtils.extractEntityHeaders(requestHeaders, result);
}
- @Override
- public InputStream getRequestHeadStream() {
+ /** Returns a stream equivalent to the given one, or null if it is empty. */
+ private InputStream peek(InputStream stream) {
+ PushbackInputStream result = new PushbackInputStream(stream);
+ try {
+ int next = result.read();
+ if (next != -1) {
+ result.unread(next);
+ return result;
+ }
+ } catch (IOException e) {
+ getLogger().log(Level.FINE, "Unable to read the request entity", e);
+ }
return null;
}
- @Override
- public OutputStream getResponseEntityStream() {
- if (responseBody == null) {
- responseBody =
- new ChannelOutputStream(
- channel, DefaultHttpContent::new, outputBufferSize, writeTimeoutMs);
+ // ------------------------------------------------------------------ response side
+
+ /**
+ * Commits the Restlet response: adds the response headers and sends the head and the entity.
+ *
+ * @param response The Restlet response.
+ */
+ void commit(Response response) {
+ try {
+ prepareEntity(response);
+ addResponseHeaders(response);
+ sendResponse(response);
+ } catch (IOException | RuntimeException e) {
+ handleException(response, e);
+ } finally {
+ complete();
+ if (response.getOnSent() != null) {
+ response.getOnSent().handle(response.getRequest(), response);
+ }
}
- return responseBody;
}
- @Override
- public Integer getSslKeySize() {
- String cipherSuite = getCipherSuite();
- return (cipherSuite != null) ? SslUtils.extractKeySize(cipherSuite) : null;
+ /** Adds the entity headers and drops the entity when the status or method forbids it. */
+ private void prepareEntity(Response response) {
+ Status status = response.getStatus();
+ Method method = response.getRequest().getMethod();
+
+ if (Method.HEAD.equals(method)) {
+ HeaderUtils.addEntityHeaders(response.getEntity(), responseHeaders);
+ response.setEntity(null);
+ } else if (Method.GET.equals(method)
+ && Status.SUCCESS_OK.equals(status)
+ && !response.isEntityAvailable()) {
+ HeaderUtils.addEntityHeaders(response.getEntity(), responseHeaders);
+ getLogger()
+ .log(
+ Level.WARNING,
+ "A response with a 200 (Ok) status should have an entity. Make sure"
+ + " that resource \"{0}\" returns one or sets the status to"
+ + " 204 (No content).",
+ response.getRequest().getResourceRef());
+ } else if (Status.SUCCESS_NO_CONTENT.equals(status)) {
+ HeaderUtils.addEntityHeaders(response.getEntity(), responseHeaders);
+ dropEntity(response, Level.FINE, "204 (No content)");
+ } else if (Status.REDIRECTION_NOT_MODIFIED.equals(status)) {
+ if (response.getEntity() != null) {
+ HeaderUtils.addNotModifiedEntityHeaders(response.getEntity(), responseHeaders);
+ response.setEntity(null);
+ }
+ } else if (Status.SUCCESS_RESET_CONTENT.equals(status)) {
+ dropEntity(response, Level.WARNING, "205 (Reset content)");
+ } else if (status.isInformational()) {
+ dropEntity(response, Level.WARNING, "informational (1xx)");
+ } else {
+ HeaderUtils.addEntityHeaders(response.getEntity(), responseHeaders);
+ if (!response.isEntityAvailable()) {
+ if (response.getEntity() != null && response.getEntity().getSize() != 0) {
+ getLogger()
+ .log(
+ Level.WARNING,
+ "A response with an unavailable and potentially non empty"
+ + " entity was returned. Ignoring the entity for"
+ + " resource \"{0}\".",
+ response.getRequest().getResourceRef());
+ }
+ response.setEntity(null);
+ }
+ }
}
- @Override
- protected byte[] getSslSessionIdBytes() {
- SSLSession session = sslSession();
- return (session != null) ? session.getId() : null;
+ private void dropEntity(Response response, Level level, String status) {
+ if (response.isEntityAvailable()) {
+ getLogger()
+ .log(
+ level,
+ "Responses with a {0} status can''t have an entity. Ignoring the"
+ + " entity for resource \"{1}\".",
+ new Object[] {status, response.getRequest().getResourceRef()});
+ }
+ response.setEntity(null);
}
- @Override
- public String getSslSessionId() {
- byte[] id = getSslSessionIdBytes();
- return (id != null && id.length > 0) ? HexFormat.of().formatHex(id) : null;
+ private void addResponseHeaders(Response response) {
+ try {
+ HeaderUtils.addGeneralHeaders(response, responseHeaders);
+ HeaderUtils.addResponseHeaders(response, responseHeaders);
+ if (response.getStatus() != null) {
+ setStatus(response.getStatus().getCode(), response.getStatus().getReasonPhrase());
+ }
+ } catch (RuntimeException e) {
+ getLogger().log(Level.WARNING, "Unable to add the response headers", e);
+ setStatus(
+ Status.SERVER_ERROR_INTERNAL.getCode(),
+ Status.SERVER_ERROR_INTERNAL.getReasonPhrase());
+ }
}
- @Override
- public boolean isConnectionBroken(Throwable exception) {
- return (exception instanceof ClosedChannelException)
- || !channel.isActive()
- || super.isConnectionBroken(exception);
+ private void setStatus(int code, String reason) {
+ this.statusCode = code;
+ this.reasonPhrase = reason;
+ }
+
+ private void handleException(Response response, Exception exception) {
+ if (isConnectionBroken(exception)) {
+ getLogger()
+ .log(
+ Level.INFO,
+ "The connection was broken. It was probably closed by the client."
+ + " Reason: {0}",
+ exception.getMessage());
+ } else if (!responseHeadWritten) {
+ getLogger()
+ .log(
+ Level.SEVERE,
+ "An exception occurred writing the response entity",
+ exception);
+ setStatus(
+ Status.SERVER_ERROR_INTERNAL.getCode(),
+ "An exception occurred writing the response entity");
+ response.setEntity(null);
+ try {
+ sendResponse(response);
+ } catch (IOException ioe) {
+ getLogger().log(Level.WARNING, "Unable to send the error response", ioe);
+ }
+ } else {
+ // The head is already on the wire: the response can only be cut short.
+ getLogger()
+ .log(
+ Level.SEVERE,
+ "An exception occurred writing the response entity",
+ exception);
+ abort();
+ }
}
- @Override
- public void sendResponse(org.restlet.Response response) throws IOException {
+ /**
+ * Sends the response head and entity.
+ *
+ * @param response The Restlet response.
+ * @throws IOException If the response could not be written.
+ */
+ private void sendResponse(Response response) throws IOException {
if (!channel.isActive()) {
throw new IOException("Connection closed by peer");
}
- boolean hasEntity = (response != null) && (response.getEntity() != null);
- writeHead(!hasEntity);
- super.sendResponse(response);
+ Representation entity = response.getEntity();
+ ConnectorService connectorService = ConnectorHelper.getConnectorService();
+ if (connectorService != null) {
+ connectorService.beforeSend(entity);
+ }
+
+ try {
+ writeHead(entity == null);
+ if (entity != null) {
+ OutputStream out = getResponseEntityStream();
+ entity.write(out);
+ out.flush();
+ out.close();
+ }
+ } finally {
+ if (entity != null) {
+ entity.release();
+ }
+ if (connectorService != null) {
+ connectorService.afterSend(entity);
+ }
+ }
+ }
+
+ private OutputStream getResponseEntityStream() {
+ if (responseBody == null) {
+ responseBody =
+ new ChannelOutputStream(
+ channel,
+ DefaultHttpContent::new,
+ helper.getHttpOutputBufferSize(),
+ helper.getConnectorIdleTimeout());
+ }
+ return responseBody;
}
/**
@@ -321,13 +505,15 @@ private void writeHead(boolean emptyBody) {
}
responseHeadWritten = true;
+ int code = statusCode;
+ String reason = reasonPhrase;
HttpResponseStatus status =
- (getReasonPhrase() != null && !getReasonPhrase().isEmpty())
- ? new HttpResponseStatus(getStatusCode(), getReasonPhrase())
- : HttpResponseStatus.valueOf(getStatusCode());
+ (reason != null && !reason.isEmpty())
+ ? new HttpResponseStatus(code, reason)
+ : HttpResponseStatus.valueOf(code);
HttpResponse head = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status);
- for (Header header : getResponseHeaders()) {
+ for (Header header : responseHeaders) {
head.headers().add(header.getName(), header.getValue());
}
@@ -336,29 +522,117 @@ private void writeHead(boolean emptyBody) {
}
boolean hasLength = head.headers().contains(HttpHeaderNames.CONTENT_LENGTH);
- boolean noBodyStatus =
- getStatusCode() == 204 || getStatusCode() == 304 || getStatusCode() < 200;
+ boolean noBodyStatus = code == 204 || code == 304 || code < 200;
- if (http2) {
- // Connection-specific headers are forbidden in HTTP/2.
+ if (kind.isStream()) {
+ // Connection-specific headers are forbidden in HTTP/2 and HTTP/3.
head.headers().remove(HttpHeaderNames.CONNECTION);
head.headers().remove(HttpHeaderNames.TRANSFER_ENCODING);
head.headers().remove("Keep-Alive");
- } else if (emptyBody) {
- if (!hasLength && !noBodyStatus && !"HEAD".equals(getMethod())) {
- head.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
+ } else {
+ if (emptyBody) {
+ if (!hasLength && !noBodyStatus && !"HEAD".equals(getMethod())) {
+ head.headers().set(HttpHeaderNames.CONTENT_LENGTH, 0);
+ }
+ } else if (!hasLength) {
+ head.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
+ }
+ if (!HttpUtil.isKeepAlive(request)) {
+ head.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
}
- } else if (!hasLength) {
- head.headers().set(HttpHeaderNames.TRANSFER_ENCODING, HttpHeaderValues.CHUNKED);
}
- if (!http2 && !HttpUtil.isKeepAlive(request)) {
- head.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE);
+ channel.write(head);
+ }
+
+ /** Completes the exchange: ends the response and frees the connection for the next request. */
+ void complete() {
+ if (completed) {
+ return;
}
+ completed = true;
- channel.write(head);
+ try {
+ if (!responseHeadWritten) {
+ writeHead(true);
+ }
+ if (responseBody != null) {
+ responseBody.close();
+ }
+ } catch (IOException e) {
+ getLogger().log(Level.FINE, "Unable to complete the response: {0}", e.getMessage());
+ }
+
+ if (!channel.isActive()) {
+ requestBody.close();
+ return;
+ }
+
+ boolean keepAlive = HttpUtil.isKeepAlive(request) && requestBody.isFullyConsumed();
+ ChannelFuture future = channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
+ requestBody.close();
+
+ if (!kind.isStream()) {
+ if (keepAlive) {
+ // Ready for the next request on this connection.
+ future.addListener(f -> channel.read());
+ } else {
+ future.addListener(ChannelFutureListener.CLOSE);
+ }
+ }
+ }
+
+ /**
+ * Aborts the exchange by closing the connection.
+ *
+ * @return True.
+ */
+ boolean abort() {
+ connection.close();
+ return true;
+ }
+
+ /**
+ * Flushes the response head, and the buffered response body if any.
+ *
+ * @throws IOException If the flush fails.
+ */
+ void flushBuffers() throws IOException {
+ if (responseBody != null) {
+ responseBody.flush();
+ } else {
+ channel.flush();
+ }
+ }
+
+ /**
+ * Indicates whether the given failure is caused by a broken connection.
+ *
+ * @param exception The failure.
+ * @return True if the failure is caused by a broken connection.
+ */
+ boolean isConnectionBroken(Throwable exception) {
+ return (exception instanceof ClosedChannelException)
+ || !channel.isActive()
+ || isBroken(exception);
}
+ private static boolean isBroken(Throwable exception) {
+ for (Throwable t = exception; t != null; t = t.getCause()) {
+ String message = t.getMessage();
+ if ((t instanceof IOException
+ && t.getClass().getName().endsWith("ClientAbortException"))
+ || (message != null
+ && (message.contains("Broken pipe")
+ || message.contains("Connection reset")))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // ------------------------------------------------------------------ TLS
+
private SSLSession sslSession() {
if (kind == StreamKind.HTTP3) {
return Http3Support.sslSession(connection);
@@ -366,4 +640,27 @@ private SSLSession sslSession() {
SslHandler ssl = connection.pipeline().get(SslHandler.class);
return (ssl != null) ? ssl.engine().getSession() : null;
}
+
+ private List getCertificates() {
+ SSLSession session = sslSession();
+ if (session != null) {
+ try {
+ return Arrays.asList(session.getPeerCertificates());
+ } catch (SSLPeerUnverifiedException _) {
+ return List.of();
+ }
+ }
+ return List.of();
+ }
+
+ private String getCipherSuite() {
+ SSLSession session = sslSession();
+ return (session != null) ? session.getCipherSuite() : null;
+ }
+
+ private String getSslSessionId() {
+ SSLSession session = sslSession();
+ byte[] id = (session != null) ? session.getId() : null;
+ return (id != null && id.length > 0) ? HexFormat.of().formatHex(id) : null;
+ }
}
diff --git a/org.restlet/src/main/java/org/restlet/engine/netty/NettyServerHelper.java b/org.restlet/src/main/java/org/restlet/engine/netty/NettyServerHelper.java
index 44e1b7b151..382b2b4264 100644
--- a/org.restlet/src/main/java/org/restlet/engine/netty/NettyServerHelper.java
+++ b/org.restlet/src/main/java/org/restlet/engine/netty/NettyServerHelper.java
@@ -52,8 +52,9 @@
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import javax.net.ssl.SSLContext;
+import org.restlet.Response;
import org.restlet.Server;
-import org.restlet.engine.adapter.HttpServerHelper;
+import org.restlet.engine.connector.ServerHelper;
import org.restlet.engine.ssl.DefaultSslContextFactory;
import org.restlet.engine.ssl.SslContextFactory;
import org.restlet.engine.ssl.SslUtils;
@@ -167,11 +168,20 @@
* | 0 |
* Graceful shutdown timeout in milliseconds. |
*
+ *
+ * | useForwardedForHeader |
+ * boolean |
+ * false |
+ * Lookup the "X-Forwarded-For" header supported by popular proxies and caches and uses it to
+ * populate the Request.getClientAddresses() method result. This information is only safe for
+ * intermediary components within your local network. Other addresses could easily be changed by
+ * setting a fake header and should not be trusted for serious security checks. |
+ *
*
*
* @author Jerome Louvel
*/
-public abstract class NettyServerHelper extends HttpServerHelper {
+public abstract class NettyServerHelper extends ServerHelper {
/** The event loop group accepting connections. */
private volatile EventLoopGroup bossGroup;
@@ -904,4 +914,17 @@ public long getShutdownTimeout() {
String getAltSvc() {
return altSvc;
}
+
+ /**
+ * Handles an exchange: converts it into a Restlet request and response, lets the server handle
+ * them, then writes the response.
+ *
+ * @param call The exchange.
+ */
+ void handle(NettyServerCall call) {
+ NettyRequest request = call.toRequest(getContext());
+ Response response = call.toResponse(request);
+ handle(request, response);
+ call.commit(response);
+ }
}
diff --git a/org.restlet/src/main/java/org/restlet/util/WrapperResponse.java b/org.restlet/src/main/java/org/restlet/util/WrapperResponse.java
index a06e96cf88..d4815add9e 100644
--- a/org.restlet/src/main/java/org/restlet/util/WrapperResponse.java
+++ b/org.restlet/src/main/java/org/restlet/util/WrapperResponse.java
@@ -20,6 +20,7 @@
import org.restlet.data.Dimension;
import org.restlet.data.MediaType;
import org.restlet.data.Method;
+import org.restlet.data.Protocol;
import org.restlet.data.Reference;
import org.restlet.data.ServerInfo;
import org.restlet.data.Status;
@@ -192,6 +193,11 @@ public Date getRetryAfter() {
return wrappedResponse.getRetryAfter();
}
+ @Override
+ public Protocol getProtocol() {
+ return getWrappedResponse().getProtocol();
+ }
+
/**
* Returns the server-specific information.
*
@@ -442,6 +448,11 @@ public void setRetryAfter(Date retryAfter) {
wrappedResponse.setRetryAfter(retryAfter);
}
+ @Override
+ public void setProtocol(Protocol protocol) {
+ getWrappedResponse().setProtocol(protocol);
+ }
+
@Override
public void setServerInfo(ServerInfo serverInfo) {
wrappedResponse.setServerInfo(serverInfo);
diff --git a/org.restlet/src/test/java/org/restlet/CallTestCase.java b/org.restlet/src/test/java/org/restlet/CallTestCase.java
index 6f2c4decc4..08527b8c7b 100644
--- a/org.restlet/src/test/java/org/restlet/CallTestCase.java
+++ b/org.restlet/src/test/java/org/restlet/CallTestCase.java
@@ -19,7 +19,7 @@
import org.restlet.data.Status;
/**
- * Test {@link org.restlet.engine.adapter.Call}.
+ * Test the properties of {@link Request} and {@link Response}.
*
* @author Lars Heuer (heuer[at]semagia.com)
*/
diff --git a/org.restlet/src/test/java/org/restlet/engine/connector/BaseConnectorsTestCase.java b/org.restlet/src/test/java/org/restlet/engine/connector/BaseConnectorsTestCase.java
index e51a446216..735e216810 100644
--- a/org.restlet/src/test/java/org/restlet/engine/connector/BaseConnectorsTestCase.java
+++ b/org.restlet/src/test/java/org/restlet/engine/connector/BaseConnectorsTestCase.java
@@ -19,7 +19,6 @@
import org.restlet.Server;
import org.restlet.data.Protocol;
import org.restlet.engine.Engine;
-import org.restlet.engine.adapter.HttpServerHelper;
/**
* Base test case that will call an abstract method for several client/server connectors
@@ -124,7 +123,7 @@ public enum HttpServer {
final ServerHelper serverHelper;
- HttpServer(HttpServerHelper serverHelper) {
+ HttpServer(ServerHelper serverHelper) {
this.serverHelper = serverHelper;
}
}
diff --git a/org.restlet/src/test/java/org/restlet/engine/connector/Http2MultiplexingTestCase.java b/org.restlet/src/test/java/org/restlet/engine/connector/Http2MultiplexingTestCase.java
index 370a86bfb2..6b6bb30b68 100644
--- a/org.restlet/src/test/java/org/restlet/engine/connector/Http2MultiplexingTestCase.java
+++ b/org.restlet/src/test/java/org/restlet/engine/connector/Http2MultiplexingTestCase.java
@@ -131,6 +131,7 @@ private void runConcurrentCalls(String baseUri) throws Exception {
new Request(
Method.GET, baseUri + "/?i=" + item));
assertEquals(Status.SUCCESS_OK, response.getStatus());
+ assertEquals("2.0", response.getProtocol().getVersion());
return response.getEntityAsText();
}));
}
diff --git a/org.restlet/src/test/java/org/restlet/engine/connector/Http3TestCase.java b/org.restlet/src/test/java/org/restlet/engine/connector/Http3TestCase.java
index 1406d7c8ed..8adea6b4e2 100644
--- a/org.restlet/src/test/java/org/restlet/engine/connector/Http3TestCase.java
+++ b/org.restlet/src/test/java/org/restlet/engine/connector/Http3TestCase.java
@@ -164,6 +164,8 @@ void getAndPutOverHttp3() throws Exception {
Response get = client.handle(new Request(Method.GET, uri("/a")));
assertEquals(Status.SUCCESS_OK, get.getStatus());
assertEquals("GET /a", get.getEntityAsText());
+ assertEquals(Protocol.HTTPS, get.getProtocol());
+ assertEquals("3.0", get.getProtocol().getVersion());
Response put =
client.handle(new Request(Method.PUT, uri("/b"), new StringRepresentation("x")));
diff --git a/org.restlet/src/test/java/org/restlet/engine/connector/ResponseProtocolTestCase.java b/org.restlet/src/test/java/org/restlet/engine/connector/ResponseProtocolTestCase.java
new file mode 100644
index 0000000000..c106324f86
--- /dev/null
+++ b/org.restlet/src/test/java/org/restlet/engine/connector/ResponseProtocolTestCase.java
@@ -0,0 +1,122 @@
+/**
+ * Copyright 2005-2026 Qlik
+ *
+ * The content of this file is subject to the terms of the Apache 2.0 open
+ * source license available at https://www.opensource.org/licenses/apache-2.0
+ *
+ * Restlet is a registered trademark of QlikTech International AB.
+ */
+package org.restlet.engine.connector;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.restlet.Client;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.Server;
+import org.restlet.data.MediaType;
+import org.restlet.data.Method;
+import org.restlet.data.Protocol;
+import org.restlet.data.Status;
+import org.restlet.engine.Engine;
+import org.restlet.util.WrapperResponse;
+
+/** Tests the protocol version recorded in the responses received by the HTTP client. */
+class ResponseProtocolTestCase {
+
+ private Server server;
+
+ private Client client;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ Engine.clearThreadLocalVariables();
+ Engine.register(false);
+ Engine.getInstance().getRegisteredServers().add(new HttpServerHelper(null));
+ Engine.getInstance().getRegisteredClients().add(new HttpClientHelper(null));
+ }
+
+ @AfterEach
+ void tearDown() throws Exception {
+ if (client != null) {
+ client.stop();
+ }
+ if (server != null) {
+ server.stop();
+ }
+ Engine.clearThreadLocalVariables();
+ }
+
+ @Test
+ void clientResponseRecordsHttp11() throws Exception {
+ server =
+ new Server(
+ Protocol.HTTP,
+ 0,
+ new Restlet() {
+ @Override
+ public void handle(Request request, Response response) {
+ response.setEntity("ok", MediaType.TEXT_PLAIN);
+ }
+ });
+ server.start();
+ client = new Client(Protocol.HTTP);
+ client.start();
+
+ Response response =
+ client.handle(
+ new Request(Method.GET, "http://localhost:" + server.getActualPort()));
+
+ assertEquals(Status.SUCCESS_OK, response.getStatus());
+ assertEquals(Protocol.HTTP, response.getProtocol());
+ assertEquals("1.1", response.getProtocol().getVersion());
+ assertEquals("HTTP/1.1 - " + Status.SUCCESS_OK, response.toString());
+ }
+
+ @Test
+ void connectorErrorKeepsRequestProtocol() throws Exception {
+ client = new Client(Protocol.HTTP);
+ client.start();
+
+ // Nothing listens on port 1.
+ Request request = new Request(Method.GET, "http://localhost:1/");
+ Response response = client.handle(request);
+
+ assertEquals(Status.CONNECTOR_ERROR_CONNECTION, response.getStatus());
+ assertSame(request.getProtocol(), response.getProtocol());
+ }
+
+ @Test
+ void defaultsToRequestProtocol() {
+ Request request = new Request(Method.GET, "https://example.com/");
+ Response response = new Response(request);
+ assertEquals(Protocol.HTTPS, response.getProtocol());
+
+ Protocol http2 = Protocol.valueOf("https", "2.0");
+ response.setProtocol(http2);
+ assertSame(http2, response.getProtocol());
+
+ response.setProtocol(null);
+ assertEquals(Protocol.HTTPS, response.getProtocol());
+
+ assertNull(new Response(null).getProtocol());
+ }
+
+ @Test
+ void wrapperDelegatesProtocol() {
+ Response wrapped = new Response(new Request(Method.GET, "http://example.com/"));
+ WrapperResponse wrapper = new WrapperResponse(wrapped);
+ Protocol http11 = Protocol.valueOf("http", "1.1");
+
+ wrapper.setProtocol(http11);
+
+ assertSame(http11, wrapped.getProtocol());
+ assertSame(http11, wrapper.getProtocol());
+ }
+}
diff --git a/org.restlet/src/test/java/org/restlet/resource/DirectoryTestCase.java b/org.restlet/src/test/java/org/restlet/resource/DirectoryTestCase.java
index 7b6a26caa8..464bd165f1 100644
--- a/org.restlet/src/test/java/org/restlet/resource/DirectoryTestCase.java
+++ b/org.restlet/src/test/java/org/restlet/resource/DirectoryTestCase.java
@@ -49,7 +49,7 @@
import org.restlet.data.Reference;
import org.restlet.data.Status;
import org.restlet.engine.Engine;
-import org.restlet.engine.adapter.HttpServerHelper;
+import org.restlet.engine.connector.HttpServerHelper;
import org.restlet.engine.header.HeaderConstants;
import org.restlet.engine.io.IoUtils;
import org.restlet.engine.local.FileClientHelper;