From 6a4648f996d5af7d0e1fe03ccab16561f0063d21 Mon Sep 17 00:00:00 2001 From: Laura Toedtli Date: Thu, 30 Jan 2025 11:30:31 +0100 Subject: [PATCH] 395606 Move Servlet/ServletFilters from StART to Scout --- .../scout/rt/app/filter/ExceptionFilter.java | 89 +++++++++++++++++++ .../org/eclipse/scout/rt/rest/log/NoLog.java | 25 ++++++ .../scout/rt/rest/log/NoLogFeature.java | 51 +++++++++++ .../scout/rt/rest/log/NoLogFilter.java | 27 ++++++ .../rt/rest/resource/ResourceHelper.java | 60 +++++++++++++ org.eclipse.scout.rt.server.commons/pom.xml | 5 ++ .../healthcheck/HealthCheckServlet.java | 3 + .../commons/servlet/ApiRestApplication.java | 22 +++++ .../commons/servlet/ExtRestApplication.java | 29 ++++++ .../filter/EnforceNoHttpSessionFilter.java | 59 ++++++++++++ .../commons/servlet/filter/LogFilter.java | 87 ++++++++++++++++++ 11 files changed, 457 insertions(+) create mode 100644 org.eclipse.scout.rt.app/src/main/java/org/eclipse/scout/rt/app/filter/ExceptionFilter.java create mode 100644 org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/log/NoLog.java create mode 100644 org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/log/NoLogFeature.java create mode 100644 org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/log/NoLogFilter.java create mode 100644 org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/resource/ResourceHelper.java create mode 100644 org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/ApiRestApplication.java create mode 100644 org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/ExtRestApplication.java create mode 100644 org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/filter/EnforceNoHttpSessionFilter.java create mode 100644 org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/filter/LogFilter.java diff --git a/org.eclipse.scout.rt.app/src/main/java/org/eclipse/scout/rt/app/filter/ExceptionFilter.java b/org.eclipse.scout.rt.app/src/main/java/org/eclipse/scout/rt/app/filter/ExceptionFilter.java new file mode 100644 index 00000000000..07d453c4d6d --- /dev/null +++ b/org.eclipse.scout.rt.app/src/main/java/org/eclipse/scout/rt/app/filter/ExceptionFilter.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) BSI Business Systems Integration AG. All rights reserved. + * http://www.bsiag.com/ + */ +package org.eclipse.scout.rt.app.filter; + +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; + +import org.eclipse.jetty.io.QuietException; +import org.eclipse.jetty.server.HttpChannel; +import org.eclipse.scout.rt.platform.context.CorrelationId; +import org.eclipse.scout.rt.platform.context.CorrelationIdContextValueProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.slf4j.MDC; + +/** + * Filter that catches Exceptions thrown by consecutive filters or servlet and that logs them along with the correlation + * id if available. Logging the correlation id is the main difference to the logging otherwise performed by Jetty. + */ +public class ExceptionFilter implements Filter { + + private static final Logger LOG = LoggerFactory.getLogger(ExceptionFilter.class); + + @Override + public void init(FilterConfig filterConfig) { + // nothing to initialize + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + try { + chain.doFilter(request, response); + } + catch (Exception e) { + if (isCausedByQuietException(e)) { + throw e; + } + + HttpServletRequest req = (HttpServletRequest) request; + MDC.put(CorrelationIdContextValueProvider.KEY, req.getHeader(CorrelationId.HTTP_HEADER_NAME)); + try { + LOG.warn(req.getRequestURI(), e); + } + finally { + MDC.remove(CorrelationIdContextValueProvider.KEY); + } + throw new JettyQuietExceptionWrapper(e); + } + } + + /** + * @return {@code true} if the given Throwable is a {@link QuietException} or wraps one. Otherwise {@code false}. + */ + protected boolean isCausedByQuietException(Throwable t) { + Throwable cur = t; + while (cur != null) { + // QuietException is a marker to use less verbosely logging + if (cur instanceof QuietException) { + return true; + } + cur = cur.getCause(); + } + return false; + } + + @Override + public void destroy() { + } + + /** + * {@link QuietException}s are not logged by Jetty unless the log level of {@link HttpChannel} is DEBUG. + */ + public static class JettyQuietExceptionWrapper extends RuntimeException implements QuietException { + private static final long serialVersionUID = 1L; + + public JettyQuietExceptionWrapper(Throwable cause) { + super(cause); + } + } +} diff --git a/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/log/NoLog.java b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/log/NoLog.java new file mode 100644 index 00000000000..8721f8486f1 --- /dev/null +++ b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/log/NoLog.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) BSI Business Systems Integration AG. All rights reserved. + * http://www.bsiag.com/ + */ +package org.eclipse.scout.rt.rest.log; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Specifies that REST calls to the annotated target should not be logged to the application's access log. + */ +@Target({TYPE, METHOD}) +@Retention(RUNTIME) +public @interface NoLog { + + /** + * @return true to disable logging for this target. This is the default value. It can be set to + * false to re-enable the logging. + */ + boolean value() default true; +} diff --git a/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/log/NoLogFeature.java b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/log/NoLogFeature.java new file mode 100644 index 00000000000..14a742695fd --- /dev/null +++ b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/log/NoLogFeature.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) BSI Business Systems Integration AG. All rights reserved. + * http://www.bsiag.com/ + */ +package org.eclipse.scout.rt.rest.log; + +import java.util.Collections; +import java.util.Set; + +import jakarta.ws.rs.container.DynamicFeature; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.FeatureContext; + +import org.eclipse.scout.rt.rest.RestApplication.IRestApplicationClassesContributor; + +/** + * Installs the {@link NoLogFilter} for all REST methods that are annotated with @{@link NoLog}. + */ +public class NoLogFeature implements DynamicFeature { + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + NoLog noLogAnnotationMethod = resourceInfo.getResourceMethod().getAnnotation(NoLog.class); + NoLog noLogAnnotationClass = resourceInfo.getResourceClass().getAnnotation(NoLog.class); + + // Compute if logging is disabled + boolean noLog = false; + if (noLogAnnotationMethod != null) { + noLog = noLogAnnotationMethod.value(); + } + else if (noLogAnnotationClass != null) { + noLog = noLogAnnotationClass.value(); + } + + // If not loggable, install a filter that sets the "NoLog" attribute + if (noLog) { + context.register(NoLogFilter.class); + } + } + + /** + * Installs the {@link NoLogFilter} into the REST application. + */ + public static class NoLogFilterFeatureContributor implements IRestApplicationClassesContributor { + + @Override + public Set> contribute() { + return Collections.singleton(NoLogFeature.class); + } + } +} diff --git a/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/log/NoLogFilter.java b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/log/NoLogFilter.java new file mode 100644 index 00000000000..f33995b07eb --- /dev/null +++ b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/log/NoLogFilter.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) BSI Business Systems Integration AG. All rights reserved. + * http://www.bsiag.com/ + */ +//FIXME +package org.eclipse.scout.rt.rest.log; + +import java.io.IOException; + +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; + +/** + * Dynamically installed request filter that sets the {@link #REQUEST_ATTRIBUTE} flag. + */ +public class NoLogFilter implements ContainerRequestFilter { + + /** + * Name of the attribute that is set to the current request by {@link NoLogFilter}. The attribute value is irrelevant. + */ + public static final String REQUEST_ATTRIBUTE = NoLogFilter.class.getName(); + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + requestContext.setProperty(REQUEST_ATTRIBUTE, "X"); + } +} diff --git a/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/resource/ResourceHelper.java b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/resource/ResourceHelper.java new file mode 100644 index 00000000000..d9a98ee046b --- /dev/null +++ b/org.eclipse.scout.rt.rest/src/main/java/org/eclipse/scout/rt/rest/resource/ResourceHelper.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) BSI Business Systems Integration AG. All rights reserved. + * http://www.bsiag.com/ + */ +package org.eclipse.scout.rt.rest.resource; + +import java.io.InputStream; + +import jakarta.ws.rs.core.CacheControl; +import jakarta.ws.rs.core.Response; + +import org.eclipse.scout.rt.platform.ApplicationScoped; +import org.eclipse.scout.rt.platform.util.IOUtility; +import org.eclipse.scout.rt.platform.util.ObjectUtility; + +/** + * Helper class used by REST resources. + */ +@ApplicationScoped +public class ResourceHelper { + + public static final String IMAGE_TYPES = "image/jpeg, image/gif, image/png"; + public static final String ZIP_TYPE = "application/zip"; + + /** + * Creates a response with streaming binary data and no-cache headers. Use this method to return large data / files. + */ + // FIXME sme check for occurrences of this header and replace by new utility (source from DownloadHttpResponseInterceptor.calcContentDispositionHeaderValue(String)). + public Response binaryResponse(InputStream in, String filename, String mimeType) { + return Response.ok(in, mimeType) + .header("Content-Disposition", "attachment; filename*=utf-8''" + IOUtility.urlEncode(filename)) + .cacheControl(createNoCacheControl()) + .build(); + } + + /** + * Creates a response with binary data and no-cache headers. + */ + public Response binaryResponse(byte[] data, String mimeType) { + return binaryResponse(data, mimeType, null); + } + + /** + * Creates a response with binary data and cache-control headers. + */ + public Response binaryResponse(byte[] data, String mimeType, CacheControl cacheControl) { + cacheControl = ObjectUtility.nvlOpt(cacheControl, this::createNoCacheControl); + return Response.ok(data, mimeType) + .cacheControl(cacheControl) + .build(); + } + + public CacheControl createNoCacheControl() { + CacheControl cc = new CacheControl(); + cc.setNoCache(true); + cc.setMustRevalidate(true); + cc.setNoStore(true); + return cc; + } +} diff --git a/org.eclipse.scout.rt.server.commons/pom.xml b/org.eclipse.scout.rt.server.commons/pom.xml index cc4d856773a..26b1a7b95d3 100644 --- a/org.eclipse.scout.rt.server.commons/pom.xml +++ b/org.eclipse.scout.rt.server.commons/pom.xml @@ -23,6 +23,11 @@ + + + org.eclipse.scout.rt + org.eclipse.scout.rt.rest + org.eclipse.scout.rt diff --git a/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/healthcheck/HealthCheckServlet.java b/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/healthcheck/HealthCheckServlet.java index ba46010a0bd..fff4f398cc8 100644 --- a/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/healthcheck/HealthCheckServlet.java +++ b/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/healthcheck/HealthCheckServlet.java @@ -20,6 +20,7 @@ import org.eclipse.scout.rt.platform.Platform; import org.eclipse.scout.rt.platform.util.LazyValue; import org.eclipse.scout.rt.platform.util.StringUtility; +import org.eclipse.scout.rt.rest.log.NoLogFilter; import org.eclipse.scout.rt.server.commons.healthcheck.IHealthChecker.IHealthCheckCategory; import org.eclipse.scout.rt.server.commons.servlet.AbstractHttpServlet; import org.eclipse.scout.rt.server.commons.servlet.HttpServletControl; @@ -52,6 +53,8 @@ public class HealthCheckServlet extends AbstractHttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + req.setAttribute(NoLogFilter.REQUEST_ATTRIBUTE, "X"); // prevent logging of calls to health servlet in LogFilter + disableCaching(req, resp); BEANS.get(HttpServletControl.class).doDefaults(this, req, resp); diff --git a/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/ApiRestApplication.java b/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/ApiRestApplication.java new file mode 100644 index 00000000000..92e50b185f8 --- /dev/null +++ b/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/ApiRestApplication.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) BSI Business Systems Integration AG. All rights reserved. + * http://www.bsiag.com/ + */ +package org.eclipse.scout.rt.server.commons.servlet; + +import org.eclipse.scout.rt.platform.util.ObjectUtility; +import org.eclipse.scout.rt.rest.RestApplication; +import org.eclipse.scout.rt.rest.RestApplicationScope; +import org.eclipse.scout.rt.rest.RestApplicationScopes; + +/** + * @see RestApplicationScopes#API + */ +public class ApiRestApplication extends RestApplication { + + @Override + protected boolean filterClass(Class clazz) { + RestApplicationScope annotation = clazz.getAnnotation(RestApplicationScope.class); + return annotation == null || ObjectUtility.isOneOf(RestApplicationScopes.API, annotation.value()); + } +} diff --git a/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/ExtRestApplication.java b/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/ExtRestApplication.java new file mode 100644 index 00000000000..c06a6102a65 --- /dev/null +++ b/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/ExtRestApplication.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) BSI Business Systems Integration AG. All rights reserved. + * http://www.bsiag.com/ + */ +package org.eclipse.scout.rt.server.commons.servlet; + +import org.eclipse.scout.rt.platform.util.ObjectUtility; +import org.eclipse.scout.rt.rest.IRestResource; +import org.eclipse.scout.rt.rest.RestApplication; +import org.eclipse.scout.rt.rest.RestApplicationScope; +import org.eclipse.scout.rt.rest.RestApplicationScopes; +import org.eclipse.scout.rt.rest.container.AntiCsrfContainerFilter; + +/** + * @see RestApplicationScopes#EXT + */ +public class ExtRestApplication extends RestApplication { + + @Override + protected boolean filterClass(Class clazz) { + return !AntiCsrfContainerFilter.class.isAssignableFrom(clazz) + && (!IRestResource.class.isAssignableFrom(clazz) || isExtScope(clazz)); + } + + protected boolean isExtScope(Class clazz) { + RestApplicationScope annotation = clazz.getAnnotation(RestApplicationScope.class); + return annotation != null && ObjectUtility.isOneOf(RestApplicationScopes.EXT, annotation.value()); + } +} diff --git a/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/filter/EnforceNoHttpSessionFilter.java b/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/filter/EnforceNoHttpSessionFilter.java new file mode 100644 index 00000000000..f5dfd0938eb --- /dev/null +++ b/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/filter/EnforceNoHttpSessionFilter.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) BSI Business Systems Integration AG. All rights reserved. + * http://www.bsiag.com/ + */ +package org.eclipse.scout.rt.server.commons.servlet.filter; + +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletRequestWrapper; +import jakarta.servlet.http.HttpSession; + +public class EnforceNoHttpSessionFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) { + // nothing to initialize + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + chain.doFilter(wrapRequest(request), response); + } + + @Override + public void destroy() { + // nothing to destroy + } + + protected HttpServletRequest wrapRequest(ServletRequest request) { + return new SessionlessHttpServletRequestWrapper((HttpServletRequest) request); + } + + public static class SessionlessHttpServletRequestWrapper extends HttpServletRequestWrapper { + + public SessionlessHttpServletRequestWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public HttpSession getSession() { + throw new UnsupportedOperationException("HTTP session not allowed"); + } + + @Override + public HttpSession getSession(boolean create) { + if (!create) { + return null; + } + throw new UnsupportedOperationException("HTTP session not allowed"); + } + } +} diff --git a/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/filter/LogFilter.java b/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/filter/LogFilter.java new file mode 100644 index 00000000000..2b7c77b11bf --- /dev/null +++ b/org.eclipse.scout.rt.server.commons/src/main/java/org/eclipse/scout/rt/server/commons/servlet/filter/LogFilter.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) BSI Business Systems Integration AG. All rights reserved. + * http://www.bsiag.com/ + */ +package org.eclipse.scout.rt.server.commons.servlet.filter; + +import java.io.IOException; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.eclipse.scout.rt.platform.util.IOUtility; +import org.eclipse.scout.rt.platform.util.ObjectUtility; +import org.eclipse.scout.rt.platform.util.StringUtility; +import org.eclipse.scout.rt.rest.log.NoLogFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link Filter} that logs all servlet requests after the request has been processed. + */ +public class LogFilter implements Filter { + + private static final Logger LOG = LoggerFactory.getLogger(LogFilter.class); + + @Override + public void init(FilterConfig filterConfig) { + // nothing to initialize + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + final HttpServletRequest httpRequest = (HttpServletRequest) request; + final HttpServletResponse httpResponse = (HttpServletResponse) response; + + long t0 = System.nanoTime(); + chain.doFilter(httpRequest, httpResponse); + if (isLoggable(httpRequest, httpResponse)) { + log(httpRequest, httpResponse, System.nanoTime() - t0); + } + } + + protected boolean isLoggable(HttpServletRequest httpRequest, HttpServletResponse httpResponse) { + if (httpRequest == null || httpRequest.getAttribute(NoLogFilter.REQUEST_ATTRIBUTE) != null) { + return false; + } + return true; + } + + protected void log(HttpServletRequest httpRequest, HttpServletResponse httpResponse, long durationNanos) { + if (LOG.isDebugEnabled()) { + LOG.debug("[{}] {} {}{} took {} ms [User-Agent: {}; Accept-Language: {}, Referer: {}; Remote-Addr: {}; X-Forwarded-For: {}]", + httpResponse.getStatus(), + httpRequest.getMethod(), + urlDecode(httpRequest.getRequestURL().toString()), + httpRequest.getQueryString() == null ? "" : "?" + urlDecode(httpRequest.getQueryString()), + StringUtility.formatNanos(durationNanos), + ObjectUtility.nvl(httpRequest.getHeader("User-Agent"), "-"), + ObjectUtility.nvl(httpRequest.getHeader("Accept-Language"), "-"), + ObjectUtility.nvl(httpRequest.getHeader("Referer"), "-"), + ObjectUtility.nvl(httpRequest.getRemoteAddr(), "-"), + ObjectUtility.nvl(httpRequest.getHeader("X-Forwarded-For"), "-")); + } + else if (LOG.isInfoEnabled()) { + LOG.info("[{}] {} {} took {} ms", + httpResponse.getStatus(), + httpRequest.getMethod(), + urlDecode(httpRequest.getRequestURL().toString()), + StringUtility.formatNanos(durationNanos)); + } + } + + protected String urlDecode(String s) { // separate method to allow customization + return IOUtility.urlDecode(s); + } + + @Override + public void destroy() { + // nothing to destroy + } +}