com.yahoo.jdisc.http.HttpRequest Maven / Gradle / Ivy
// Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.jdisc.http;
import com.yahoo.jdisc.HeaderFields;
import com.yahoo.jdisc.Request;
import com.yahoo.jdisc.handler.CompletionHandler;
import com.yahoo.jdisc.handler.ContentChannel;
import com.yahoo.jdisc.handler.RequestHandler;
import com.yahoo.jdisc.handler.ResponseHandler;
import com.yahoo.jdisc.service.CurrentContainer;
import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.UrlEncoded;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.URI;
import java.security.Principal;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* A HTTP request.
*
* @author Anirudha Khanna
* @author Einar M R Rosenvinge
*/
public class HttpRequest extends Request {
public enum Method {
OPTIONS,
GET,
HEAD,
POST,
PUT,
PATCH,
DELETE,
TRACE,
CONNECT
}
public enum Version {
HTTP_1_0("HTTP/1.0"),
HTTP_1_1("HTTP/1.1"),
HTTP_2_0("HTTP/2.0");
private final String str;
Version(String str) {
this.str = str;
}
@Override
public String toString() {
return str;
}
public static Version fromString(String str) {
for (Version version : values()) {
if (version.str.equals(str)) {
return version;
}
}
throw new IllegalArgumentException(str);
}
}
private final long jvmRelativeCreatedAt = System.nanoTime();
private final HeaderFields trailers = new HeaderFields();
private final Map> parameters = new HashMap<>();
private volatile Principal principal;
private final long connectedAt;
private Method method;
private Version version;
private SocketAddress remoteAddress;
private URI proxyServer;
private Long connectionTimeout;
protected HttpRequest(CurrentContainer container, URI uri, Method method, Version version,
SocketAddress remoteAddress, Long connectedAtMillis, long creationTime)
{
super(container, uri, true, creationTime);
try {
this.method = method;
this.version = version;
this.remoteAddress = remoteAddress;
this.parameters.putAll(getUriQueryParameters(uri));
if (connectedAtMillis != null) {
this.connectedAt = connectedAtMillis;
} else {
this.connectedAt = creationTime(TimeUnit.MILLISECONDS);
}
} catch (Throwable e) {
release();
throw e;
}
}
private HttpRequest(Request parent, URI uri, Method method, Version version) {
super(parent, uri);
try {
this.method = method;
this.version = version;
this.remoteAddress = null;
this.parameters.putAll(getUriQueryParameters(uri));
this.connectedAt = creationTime(TimeUnit.MILLISECONDS);
} catch (Throwable e) {
release();
throw e;
}
}
private static Map> getUriQueryParameters(URI uri) {
if (uri.getRawQuery() == null) return Map.of();
MultiMap params = new MultiMap<>();
UrlEncoded.decodeUtf8To(uri.getRawQuery(), params);
return Map.copyOf(params);
}
public Method getMethod() {
return method;
}
public void setMethod(Method method) {
this.method = method;
}
public Version getVersion() {
return version;
}
/** Returns the remote address, or null if unresolved */
public String getRemoteHostAddress() {
if (remoteAddress instanceof InetSocketAddress) {
InetAddress remoteInetAddress = ((InetSocketAddress) remoteAddress).getAddress();
if (remoteInetAddress == null)
return null;
return remoteInetAddress.getHostAddress();
}
else {
throw new RuntimeException("Unknown SocketAddress class: " + remoteAddress.getClass().getName());
}
}
public String getRemoteHostName() {
if (remoteAddress instanceof InetSocketAddress) {
InetAddress remoteInetAddress = ((InetSocketAddress) remoteAddress).getAddress();
if (remoteInetAddress == null) return null; // not resolved; we have no network
return remoteInetAddress.getHostName();
}
else {
throw new RuntimeException("Unknown SocketAddress class: " + remoteAddress.getClass().getName());
}
}
public int getRemotePort() {
if (remoteAddress instanceof InetSocketAddress)
return ((InetSocketAddress) remoteAddress).getPort();
else
throw new RuntimeException("Unknown SocketAddress class: " + remoteAddress.getClass().getName());
}
public void setVersion(Version version) {
this.version = version;
}
public SocketAddress getRemoteAddress() {
return remoteAddress;
}
public void setRemoteAddress(SocketAddress remoteAddress) {
this.remoteAddress = remoteAddress;
}
public URI getProxyServer() {
return proxyServer;
}
public void setProxyServer(URI proxyServer) {
this.proxyServer = proxyServer;
}
/**
* For server requests, this returns the timestamp of when the underlying HTTP channel was connected.
*
*
For client requests, this returns the same value as {@link #creationTime(java.util.concurrent.TimeUnit)}.
*
* @param unit the unit to return the time in
* @return the timestamp of when the underlying HTTP channel was connected, or request creation time
*/
public long getConnectedAt(TimeUnit unit) {
return unit.convert(connectedAt, TimeUnit.MILLISECONDS);
}
public Long getConnectionTimeout(TimeUnit unit) {
if (connectionTimeout == null) {
return null;
}
return unit.convert(connectionTimeout, TimeUnit.MILLISECONDS);
}
/**
* Sets the allocated time that this HttpRequest is allowed to spend trying to connect to a remote host. This has
* no effect on an HttpRequest received by a {@link RequestHandler}. If no connection timeout is assigned to an
* HttpRequest, it defaults the connection-timeout in the client configuration.
*
* NOTE: Where {@link Request#setTimeout(long, TimeUnit)} sets the expiration time between calling a
* RequestHandler and a {@link ResponseHandler}, this method sets the expiration time of the connect-operation as
* performed by the client.
*
* @param timeout The allocated amount of time.
* @param unit The time unit of the timeout argument.
*/
public void setConnectionTimeout(long timeout, TimeUnit unit) {
this.connectionTimeout = unit.toMillis(timeout);
}
public Map> parameters() {
return parameters;
}
public void copyHeaders(HeaderFields target) {
target.addAll(headers());
}
public List decodeCookieHeader() {
List cookies = headers().get(HttpHeaders.Names.COOKIE);
if (cookies == null) {
return List.of();
}
List ret = new LinkedList<>();
for (String cookie : cookies) {
ret.addAll(Cookie.fromCookieHeader(cookie));
}
return ret;
}
public void encodeCookieHeader(List cookies) {
headers().put(HttpHeaders.Names.COOKIE, Cookie.toCookieHeader(cookies));
}
/**
* Returns the set of trailer header fields of this HttpRequest. These are typically meta-data that should have
* been part of {@link #headers()}, but were not available prior to calling {@link #connect(ResponseHandler)}. You
* must NOT WRITE to these headers AFTER calling {@link ContentChannel#close(CompletionHandler)}, and you must NOT
* READ from these headers BEFORE {@link ContentChannel#close(CompletionHandler)} has been called.
*
* NOTE: These headers are NOT thread-safe. You need to explicitly synchronized on the returned object to
* prevent concurrency issues such as ConcurrentModificationExceptions.
*
* @return The trailer headers of this HttpRequest.
*/
public HeaderFields trailers() {
return trailers;
}
/**
* Returns whether this request was explicitly chunked from the client. NOTE that there are cases
* where the underlying HTTP server library (Netty for the time being) will read the request in a chunked manner. An
* application MUST wait for {@link com.yahoo.jdisc.handler.ContentChannel#close(com.yahoo.jdisc.handler.CompletionHandler)}
* before it can actually know that it has received the entire request.
*
* @return true if this request was chunked from the client.
*/
public boolean isChunked() {
return version == Version.HTTP_1_1 &&
headers().containsIgnoreCase(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED);
}
public boolean hasChunkedResponse() {
return version == Version.HTTP_1_1 &&
!headers().isTrue(HttpHeaders.Names.X_DISABLE_CHUNKING);
}
public boolean isKeepAlive() {
if (headers().containsIgnoreCase(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.KEEP_ALIVE)) {
return true;
}
if (headers().containsIgnoreCase(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE)) {
return false;
}
return version == Version.HTTP_1_1;
}
/**
* @return the relative created timestamp (using {@link System#nanoTime()}
*/
public long relativeCreatedAtNanoTime() { return jvmRelativeCreatedAt; }
public Principal getUserPrincipal() {
return principal;
}
public void setUserPrincipal(Principal principal) {
this.principal = principal;
}
public static HttpRequest newServerRequest(CurrentContainer container, URI uri) {
return newServerRequest(container, uri, Method.GET);
}
public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method) {
return newServerRequest(container, uri, method, Version.HTTP_1_1);
}
public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method, Version version) {
return newServerRequest(container, uri, method, version, null);
}
public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method, Version version,
SocketAddress remoteAddress) {
return new HttpRequest(container, uri, method, version, remoteAddress, null, -1);
}
public static HttpRequest newServerRequest(CurrentContainer container, URI uri, Method method, Version version,
SocketAddress remoteAddress, long connectedAtMillis, long creationTime)
{
return new HttpRequest(container, uri, method, version, remoteAddress, connectedAtMillis, creationTime);
}
public static HttpRequest newClientRequest(Request parent, URI uri) {
return newClientRequest(parent, uri, Method.GET);
}
public static HttpRequest newClientRequest(Request parent, URI uri, Method method) {
return newClientRequest(parent, uri, method, Version.HTTP_1_1);
}
public static HttpRequest newClientRequest(Request parent, URI uri, Method method, Version version) {
return new HttpRequest(parent, uri, method, version);
}
}