diff options
Diffstat (limited to 'container-core')
10 files changed, 112 insertions, 12 deletions
diff --git a/container-core/abi-spec.json b/container-core/abi-spec.json index aae5ea31fbb..d07d21ae71f 100644 --- a/container-core/abi-spec.json +++ b/container-core/abi-spec.json @@ -660,6 +660,10 @@ "public com.yahoo.container.jdisc.HttpRequestBuilder withRequestContent(java.io.InputStream)", "public com.yahoo.container.jdisc.HttpRequestBuilder withScheme(java.lang.String)", "public com.yahoo.container.jdisc.HttpRequestBuilder withHostname(java.lang.String)", + "public com.yahoo.container.jdisc.HttpRequestBuilder withPrincipal(java.security.Principal)", + "public com.yahoo.container.jdisc.HttpRequestBuilder withRemoteAddress(java.net.SocketAddress)", + "public com.yahoo.container.jdisc.HttpRequestBuilder withAttribute(java.lang.String, java.lang.Object)", + "public com.yahoo.container.jdisc.HttpRequestBuilder withPort(int)", "public com.yahoo.container.jdisc.HttpRequest build()" ], "fields" : [ ] @@ -1089,6 +1093,7 @@ "public com.yahoo.jdisc.http.ConnectorConfig$Builder maxContentSizeErrorMessageTemplate(java.lang.String)", "public com.yahoo.jdisc.http.ConnectorConfig$Builder reuseAddress(boolean)", "public com.yahoo.jdisc.http.ConnectorConfig$Builder idleTimeout(double)", + "public com.yahoo.jdisc.http.ConnectorConfig$Builder shutdownIdleTimeout(double)", "public com.yahoo.jdisc.http.ConnectorConfig$Builder tcpKeepAliveEnabled(boolean)", "public com.yahoo.jdisc.http.ConnectorConfig$Builder tcpNoDelay(boolean)", "public com.yahoo.jdisc.http.ConnectorConfig$Builder throttling(com.yahoo.jdisc.http.ConnectorConfig$Throttling$Builder)", @@ -1475,6 +1480,7 @@ "public java.lang.String maxContentSizeErrorMessageTemplate()", "public boolean reuseAddress()", "public double idleTimeout()", + "public double shutdownIdleTimeout()", "public boolean tcpKeepAliveEnabled()", "public boolean tcpNoDelay()", "public com.yahoo.jdisc.http.ConnectorConfig$Throttling throttling()", diff --git a/container-core/src/main/java/com/yahoo/container/jdisc/HttpRequestBuilder.java b/container-core/src/main/java/com/yahoo/container/jdisc/HttpRequestBuilder.java index a2d792e6ae0..147f388e08c 100644 --- a/container-core/src/main/java/com/yahoo/container/jdisc/HttpRequestBuilder.java +++ b/container-core/src/main/java/com/yahoo/container/jdisc/HttpRequestBuilder.java @@ -4,6 +4,8 @@ package com.yahoo.container.jdisc; import com.yahoo.jdisc.http.HttpRequest.Method; import java.io.InputStream; +import java.net.SocketAddress; +import java.security.Principal; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -19,9 +21,13 @@ public class HttpRequestBuilder { private final String path; private final Map<String, List<String>> queryParameters = new TreeMap<>(); private final Map<String, String> headers = new TreeMap<>(); + private final Map<String, Object> attributes = new TreeMap<>(); private String scheme; private String hostname; private InputStream content; + private Principal principal; + private SocketAddress socketAddress; + private int port = -1; private HttpRequestBuilder(Method method, String path) { this.method = method; @@ -43,10 +49,20 @@ public class HttpRequestBuilder { public HttpRequestBuilder withHostname(String hostname) { this.hostname = hostname; return this; } + public HttpRequestBuilder withPrincipal(Principal p) { principal = p; return this; } + + public HttpRequestBuilder withRemoteAddress(SocketAddress sa) { socketAddress = sa; return this; } + + public HttpRequestBuilder withAttribute(String name, Object value) { attributes.put(name, value); return this; } + + public HttpRequestBuilder withPort(int port) { this.port = port; return this; } + public HttpRequest build() { String scheme = this.scheme != null ? this.scheme : "http"; String hostname = this.hostname != null ? this.hostname : "localhost"; - StringBuilder uriBuilder = new StringBuilder(scheme).append("://").append(hostname).append(path); + StringBuilder uriBuilder = new StringBuilder(scheme).append("://").append(hostname); + if (port > 0) uriBuilder.append(':').append(port); + uriBuilder.append(path); if (queryParameters.size() > 0) { uriBuilder.append('?'); queryParameters.forEach((name, values) -> { @@ -66,6 +82,9 @@ public class HttpRequestBuilder { request = HttpRequest.createTestRequest(uriBuilder.toString(), method); } headers.forEach((name, value) -> request.getJDiscRequest().headers().put(name, value)); + if (principal != null) request.getJDiscRequest().setUserPrincipal(principal); + if (socketAddress != null) request.getJDiscRequest().setRemoteAddress(socketAddress); + request.getJDiscRequest().context().putAll(attributes); return request; } } diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java index 983adec034d..3159766981c 100644 --- a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/JDiscServerConnector.java @@ -51,7 +51,11 @@ class JDiscServerConnector extends ServerConnector { setName(config.name()); setAcceptQueueSize(config.acceptQueueSize()); setReuseAddress(config.reuseAddress()); - setIdleTimeout((long) (config.idleTimeout() * 1000)); + long idleTimeout = (long)(config.idleTimeout() * 1000); + setIdleTimeout(idleTimeout); + long shutdownIdleTimeout = (long) (config.shutdownIdleTimeout() * 1000); + // Ensure shutdown idle timeout is less than idle timeout and stop timeout + setShutdownIdleTimeout(Math.min(shutdownIdleTimeout, Math.min(idleTimeout, server.getStopTimeout()))); } public ConnectionStatistics getStatistics() { diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java index a022d208d05..9fee54dd1d4 100644 --- a/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java +++ b/container-core/src/main/java/com/yahoo/jdisc/http/server/jetty/ServletRequestReader.java @@ -1,7 +1,6 @@ // Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.jdisc.http.server.jetty; -import com.yahoo.jdisc.Response; import com.yahoo.jdisc.handler.CompletionHandler; import com.yahoo.jdisc.handler.ContentChannel; import com.yahoo.jdisc.http.ConnectorConfig; @@ -19,6 +18,8 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.Logger; +import static com.yahoo.jdisc.Response.Status.REQUEST_TOO_LONG; + /** * Finished when either * 1) There was an error @@ -111,7 +112,8 @@ class ServletRequestReader { long maxContentSize = resolveMaxContentSize(cfg); var msgTemplate = resolveMaxContentSizeErrorMessage(cfg); this.requestContentChannel = maxContentSize >= 0 - ? new ByteLimitedContentChannel(Objects.requireNonNull(requestContentChannel), maxContentSize, msgTemplate) + ? new ByteLimitedContentChannel( + Objects.requireNonNull(requestContentChannel), maxContentSize, msgTemplate, req.getContentLengthLong()) : Objects.requireNonNull(requestContentChannel); this.janitor = Objects.requireNonNull(janitor); this.metricReporter = Objects.requireNonNull(metricReporter); @@ -285,24 +287,29 @@ class ServletRequestReader { private static class ByteLimitedContentChannel implements ContentChannel { private final long maxContentSize; private final String messageTemplate; + private final long contentLengthHeader; private final AtomicLong bytesWritten = new AtomicLong(); private final ContentChannel delegate; - ByteLimitedContentChannel(ContentChannel delegate, long maxContentSize, String messageTemplate) { + ByteLimitedContentChannel(ContentChannel delegate, long maxContentSize, String messageTemplate, long contentLengthHeader) { this.delegate = delegate; this.maxContentSize = maxContentSize; this.messageTemplate = messageTemplate; + this.contentLengthHeader = contentLengthHeader; } @Override public void write(ByteBuffer buf, CompletionHandler handler) { long written = bytesWritten.addAndGet(buf.remaining()); - if (written > maxContentSize) { + if (contentLengthHeader != -1 && contentLengthHeader > maxContentSize) { handler.failed(new RequestException( - Response.Status.REQUEST_TOO_LONG, messageTemplate.formatted(written, maxContentSize))); - return; + REQUEST_TOO_LONG, messageTemplate.formatted(contentLengthHeader, maxContentSize))); + } else if (written > maxContentSize) { + handler.failed(new RequestException( + REQUEST_TOO_LONG, messageTemplate.formatted(written, maxContentSize))); + } else { + delegate.write(buf, handler); } - delegate.write(buf, handler); } @Override public void close(CompletionHandler h) { delegate.close(h); } diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApi.java b/container-core/src/main/java/com/yahoo/restapi/RestApi.java index 18d8d8c49b4..ee5628988c9 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApi.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApi.java @@ -15,6 +15,7 @@ import com.yahoo.security.tls.ConnectionAuthContext; import javax.net.ssl.SSLSession; import java.io.InputStream; +import java.net.InetSocketAddress; import java.security.Principal; import java.util.List; import java.util.Optional; @@ -153,6 +154,7 @@ public interface RestApi { Principal userPrincipalOrThrow(); Optional<SSLSession> sslSession(); Optional<ConnectionAuthContext> connectionAuthContext(); + InetSocketAddress remoteAddress(); interface Parameters { Optional<String> getString(String name); @@ -193,6 +195,7 @@ public interface RestApi { interface FilterContext { RequestContext requestContext(); String route(); + void setPrincipal(Principal principal); HttpResponse executeNext(); } } diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApiException.java b/container-core/src/main/java/com/yahoo/restapi/RestApiException.java index e3acf4258f1..3a44bc4da5f 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApiException.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApiException.java @@ -57,6 +57,7 @@ public class RestApiException extends RuntimeException { } public static class BadRequest extends RestApiException { + public BadRequest() { this("Bad request"); } public BadRequest(String message) { this(message, null); } public BadRequest(Throwable cause) { this(cause.getMessage(), cause); } public BadRequest(String message, Throwable cause) { super(ErrorResponse::badRequest, message, cause); } @@ -69,6 +70,7 @@ public class RestApiException extends RuntimeException { } public static class Forbidden extends RestApiException { + public Forbidden() { this("Forbidden"); } public Forbidden(String message) { super(ErrorResponse::forbidden, message, null); } public Forbidden(String message, Throwable cause) { super(ErrorResponse::forbidden, message, cause); } } diff --git a/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java b/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java index 39dd17d3563..090e06c221f 100644 --- a/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java +++ b/container-core/src/main/java/com/yahoo/restapi/RestApiImpl.java @@ -21,6 +21,7 @@ import com.yahoo.security.tls.TransportSecurityUtils; import javax.net.ssl.SSLSession; import java.io.InputStream; +import java.net.InetSocketAddress; import java.net.URI; import java.security.Principal; import java.util.ArrayList; @@ -76,7 +77,11 @@ class RestApiImpl implements RestApi { resolvedRoute, requestContext, filters, createFilterContextRecursive(resolvedRoute, requestContext, resolvedRoute.filters, null)); if (filterContext != null) { - return filterContext.executeFirst(); + try { + return filterContext.executeFirst(); + } catch (RuntimeException e) { + return mapException(requestContext, e); + } } else { return dispatchToRoute(resolvedRoute, requestContext); } @@ -487,7 +492,7 @@ class RestApiImpl implements RestApi { @Override public Optional<ConnectionAuthContext> connectionAuthContext() { return sslSession().flatMap(TransportSecurityUtils::getConnectionAuthContext); } - + @Override public InetSocketAddress remoteAddress() { return (InetSocketAddress) request.getJDiscRequest().getRemoteAddress(); } private class PathParametersImpl implements RestApi.RequestContext.PathParameters { @Override @@ -496,7 +501,7 @@ class RestApiImpl implements RestApi { } @Override public String getStringOrThrow(String name) { return getString(name) - .orElseThrow(() -> new RestApiException.BadRequest("Path parameter '" + name + "' is missing")); + .orElseThrow(() -> new RestApiException.NotFound("Path parameter '" + name + "' is missing")); } @Override public HttpURL.Path getFullPath() { return pathMatcher.getPath(); @@ -554,6 +559,7 @@ class RestApiImpl implements RestApi { @Override public RestApi.RequestContext requestContext() { return requestContext; } @Override public String route() { return route.name != null ? route.name : route.pathPattern; } + @Override public void setPrincipal(Principal p) { requestContext.request.getJDiscRequest().setUserPrincipal(p); } HttpResponse executeFirst() { return filter.filterRequest(this); } diff --git a/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def b/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def index d2f081fe7d5..5e59d998e86 100644 --- a/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def +++ b/container-core/src/main/resources/configdefinitions/jdisc.http.jdisc.http.connector.def @@ -33,6 +33,9 @@ reuseAddress bool default=true # The maximum idle time for a connection, which roughly translates to the Socket.setSoTimeout(int). idleTimeout double default=180.0 +# The idle timeout that takes effect during graceful shutdown of Jetty +shutdownIdleTimeout double default=5.0 + # TODO Vespa 9 Remove # Has no effect since Jetty 11 upgrade tcpKeepAliveEnabled bool default=false diff --git a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java index 6e218a6ab66..adb35db8ebf 100644 --- a/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java +++ b/container-core/src/test/java/com/yahoo/jdisc/http/server/jetty/HttpServerTest.java @@ -160,6 +160,20 @@ public class HttpServerTest { } @Test + void requireThatTooLargePayloadFailsWith413() throws Exception { + final JettyTestDriver driver = JettyTestDriver.newConfiguredInstance( + new EchoRequestHandler(), + new ServerConfig.Builder(), + new ConnectorConfig.Builder() + .maxContentSize(100)); + driver.client().newPost("/status.html") + .setBinaryContent(new byte[200]) + .execute() + .expectStatusCode(is(REQUEST_TOO_LONG)); + assertTrue(driver.close()); + } + + @Test void requireThatMultipleHostHeadersReturns400() throws Exception { var metricConsumer = new MetricConsumerMock(); JettyTestDriver driver = JettyTestDriver.newConfiguredInstance( diff --git a/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java b/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java index bf0ccb95887..d27e04bbd7a 100644 --- a/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java +++ b/container-core/src/test/java/com/yahoo/restapi/RestApiImplTest.java @@ -158,6 +158,42 @@ class RestApiImplTest { assertRequiredCapability(restApi, Method.POST, "/api2", Capability.CONTENT__DOCUMENT_API); } + @Test + void maps_exception_for_filter_throwing() { + RestApi.Filter throwingFilter = (ctx) -> { + throw new RestApiException.Forbidden("forbidden"); + }; + var restApi = RestApi.builder() + .setDefaultRoute(route("{*}").defaultHandler(ctx -> "hello world")) + .addFilter(throwingFilter) + .build(); + verifyJsonResponse(restApi, Method.GET, "/", null, 403, "{\"error-code\":\"FORBIDDEN\",\"message\":\"forbidden\"}"); + } + + @Test + void missing_parameters_are_mapped_to_4xx_response() { + var restApi = RestApi.builder() + .addRoute(route("/missing-path-param").get(ctx -> ctx.pathParameters().getStringOrThrow("missing"))) + .addRoute(route("/missing-query-param").get(ctx -> ctx.queryParameters().getStringOrThrow("missing"))) + .build(); + verifyJsonResponse(restApi, Method.GET, "/missing-path-param", null, 404, + "{\"error-code\":\"NOT_FOUND\",\"message\":\"Path parameter 'missing' is missing\"}"); + verifyJsonResponse(restApi, Method.GET, "/missing-query-param", null, 400, + "{\"error-code\":\"BAD_REQUEST\",\"message\":\"Query parameter 'missing' is missing\"}"); + } + + @Test + void principal_from_filter_is_visible_to_handler() { + var restApi = RestApi.builder() + .addRoute(route("/api1").get(ctx -> ctx.userPrincipalOrThrow().getName())) + .addFilter(ctx -> { + ctx.setPrincipal(() -> "my-principal-name"); + return ctx.executeNext(); + }) + .build(); + verifyJsonResponse(restApi, Method.GET, "/api1", null, 200, "{\"message\":\"my-principal-name\"}"); + } + private static void verifyJsonResponse( RestApi restApi, Method method, String path, String requestContent, int expectedStatusCode, String expectedJson) { |