From 7a15067878004c3c456099d45a38d3424bdd74e0 Mon Sep 17 00:00:00 2001 From: Jerome Louvel <374450+jlouvel@users.noreply.github.com> Date: Sat, 17 Oct 2026 23:05:28 -0400 Subject: [PATCH] Remove engine.adapter and map Netty messages directly The Netty connectors now build Restlet requests and responses directly from Netty messages, without the ServerCall/ClientCall and ServerAdapter/ ClientAdapter layer. - NettyServerHelper extends ServerHelper, NettyClientHelper extends ClientHelper (two fewer inheritance levels, Sonar S110). - NettyServerCall and the new NettyRequest take over ServerCall, ServerAdapter and HttpRequest: lazy header parsing, headers and version attributes, TLS client info, entity rules per status and method, ConnectorService callbacks. - NettyClientCall takes over ClientCall and ClientAdapter. - Removed the org.restlet.engine.adapter package and the "adapter" connector parameter (breaking change, 3.0). - Added Response.getProtocol()/setProtocol(): the HTTP client connector records the protocol version the server replied with (1.1, 2.0, 3.0). WrapperResponse delegates it; Response.toString() shows it. Signed-off-by: Jerome Louvel <374450+jlouvel@users.noreply.github.com> --- changes.md | 12 + org.restlet/src/main/java/module-info.java | 1 - .../src/main/java/org/restlet/Response.java | 29 +- .../org/restlet/engine/adapter/Adapter.java | 51 -- .../java/org/restlet/engine/adapter/Call.java | 481 ------------- .../restlet/engine/adapter/ClientAdapter.java | 173 ----- .../restlet/engine/adapter/ClientCall.java | 287 -------- .../engine/adapter/HttpClientHelper.java | 114 ---- .../restlet/engine/adapter/HttpRequest.java | 644 ------------------ .../restlet/engine/adapter/HttpResponse.java | 84 --- .../engine/adapter/HttpServerHelper.java | 133 ---- .../restlet/engine/adapter/ServerAdapter.java | 259 ------- .../restlet/engine/adapter/ServerCall.java | 538 --------------- .../restlet/engine/adapter/package-info.java | 6 - .../restlet/engine/netty/NettyClientCall.java | 241 +++++-- .../engine/netty/NettyClientHelper.java | 33 +- .../restlet/engine/netty/NettyRequest.java | 457 +++++++++++++ .../restlet/engine/netty/NettyServerCall.java | 637 ++++++++++++----- .../engine/netty/NettyServerHelper.java | 27 +- .../org/restlet/util/WrapperResponse.java | 11 + .../test/java/org/restlet/CallTestCase.java | 2 +- .../connector/BaseConnectorsTestCase.java | 3 +- .../connector/Http2MultiplexingTestCase.java | 1 + .../engine/connector/Http3TestCase.java | 2 + .../connector/ResponseProtocolTestCase.java | 122 ++++ .../restlet/resource/DirectoryTestCase.java | 2 +- 26 files changed, 1346 insertions(+), 3004 deletions(-) delete mode 100644 org.restlet/src/main/java/org/restlet/engine/adapter/Adapter.java delete mode 100644 org.restlet/src/main/java/org/restlet/engine/adapter/Call.java delete mode 100644 org.restlet/src/main/java/org/restlet/engine/adapter/ClientAdapter.java delete mode 100644 org.restlet/src/main/java/org/restlet/engine/adapter/ClientCall.java delete mode 100644 org.restlet/src/main/java/org/restlet/engine/adapter/HttpClientHelper.java delete mode 100644 org.restlet/src/main/java/org/restlet/engine/adapter/HttpRequest.java delete mode 100644 org.restlet/src/main/java/org/restlet/engine/adapter/HttpResponse.java delete mode 100644 org.restlet/src/main/java/org/restlet/engine/adapter/HttpServerHelper.java delete mode 100644 org.restlet/src/main/java/org/restlet/engine/adapter/ServerAdapter.java delete mode 100644 org.restlet/src/main/java/org/restlet/engine/adapter/ServerCall.java delete mode 100644 org.restlet/src/main/java/org/restlet/engine/adapter/package-info.java create mode 100644 org.restlet/src/main/java/org/restlet/engine/netty/NettyRequest.java create mode 100644 org.restlet/src/test/java/org/restlet/engine/connector/ResponseProtocolTestCase.java 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 nameValue typeDefault valueDescription
adapterStringorg.restlet.engine.adapter.ClientAdapterClass 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 nameValue typeDefault valueDescription
useForwardedForHeaderbooleanfalseLookup 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.
adapterStringorg.restlet.engine.adapter.ServerAdapterClass 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;