diff --git a/src/main/java/stirling/software/SPDF/config/AppConfig.java b/src/main/java/stirling/software/SPDF/config/AppConfig.java index 9212bf7e..3fd65261 100644 --- a/src/main/java/stirling/software/SPDF/config/AppConfig.java +++ b/src/main/java/stirling/software/SPDF/config/AppConfig.java @@ -1,53 +1,53 @@ -package stirling.software.SPDF.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class AppConfig { - - - @Bean(name = "loginEnabled") - public boolean loginEnabled() { - String appName = System.getProperty("login.enabled"); - if (appName == null) - appName = System.getenv("login.enabled"); - - return (appName != null) ? Boolean.valueOf(appName) : false; - } - - @Bean(name = "appName") - public String appName() { - String appName = System.getProperty("APP_HOME_NAME"); - if (appName == null) - appName = System.getenv("APP_HOME_NAME"); - return (appName != null) ? appName : "Stirling PDF"; - } - - @Bean(name = "appVersion") - public String appVersion() { - String version = getClass().getPackage().getImplementationVersion(); - return (version != null) ? version : "0.0.0"; - } - - @Bean(name = "homeText") - public String homeText() { - String homeText = System.getProperty("APP_HOME_DESCRIPTION"); - if (homeText == null) - homeText = System.getenv("APP_HOME_DESCRIPTION"); - return (homeText != null) ? homeText : "null"; - } - - @Bean(name = "navBarText") - public String navBarText() { - String navBarText = System.getProperty("APP_NAVBAR_NAME"); - if (navBarText == null) - navBarText = System.getenv("APP_NAVBAR_NAME"); - if (navBarText == null) - navBarText = System.getProperty("APP_HOME_NAME"); - if (navBarText == null) - navBarText = System.getenv("APP_HOME_NAME"); - - return (navBarText != null) ? navBarText : "Stirling PDF"; - } +package stirling.software.SPDF.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AppConfig { + + + @Bean(name = "loginEnabled") + public boolean loginEnabled() { + String appName = System.getProperty("login.enabled"); + if (appName == null) + appName = System.getenv("login.enabled"); + + return (appName != null) ? Boolean.valueOf(appName) : false; + } + + @Bean(name = "appName") + public String appName() { + String appName = System.getProperty("APP_HOME_NAME"); + if (appName == null) + appName = System.getenv("APP_HOME_NAME"); + return (appName != null) ? appName : "Stirling PDF"; + } + + @Bean(name = "appVersion") + public String appVersion() { + String version = getClass().getPackage().getImplementationVersion(); + return (version != null) ? version : "0.0.0"; + } + + @Bean(name = "homeText") + public String homeText() { + String homeText = System.getProperty("APP_HOME_DESCRIPTION"); + if (homeText == null) + homeText = System.getenv("APP_HOME_DESCRIPTION"); + return (homeText != null) ? homeText : "null"; + } + + @Bean(name = "navBarText") + public String navBarText() { + String navBarText = System.getProperty("APP_NAVBAR_NAME"); + if (navBarText == null) + navBarText = System.getenv("APP_NAVBAR_NAME"); + if (navBarText == null) + navBarText = System.getProperty("APP_HOME_NAME"); + if (navBarText == null) + navBarText = System.getenv("APP_HOME_NAME"); + + return (navBarText != null) ? navBarText : "Stirling PDF"; + } } \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java index dc2e144b..430ef5ea 100644 --- a/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/CleanUrlInterceptor.java @@ -1,83 +1,83 @@ -package stirling.software.SPDF.config; - -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.ModelAndView; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.Arrays; -import java.util.List; -import java.util.HashMap; -import java.util.Map; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.ModelAndView; - -public class CleanUrlInterceptor implements HandlerInterceptor { - - private static final List ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error"); - - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - String queryString = request.getQueryString(); - if (queryString != null && !queryString.isEmpty()) { - String requestURI = request.getRequestURI(); - - Map parameters = new HashMap<>(); - - // Keep only the allowed parameters - String[] queryParameters = queryString.split("&"); - for (String param : queryParameters) { - String[] keyValue = param.split("="); - System.out.print("astirli " + keyValue[0]); - if (keyValue.length != 2) { - continue; - } - System.out.print("astirli2 " + keyValue[0]); - - if (ALLOWED_PARAMS.contains(keyValue[0])) { - parameters.put(keyValue[0], keyValue[1]); - } - } - - // If there are any parameters that are not allowed - if (parameters.size() != queryParameters.length) { - // Construct new query string - StringBuilder newQueryString = new StringBuilder(); - for (Map.Entry entry : parameters.entrySet()) { - if (newQueryString.length() > 0) { - newQueryString.append("&"); - } - newQueryString.append(entry.getKey()).append("=").append(entry.getValue()); - } - - // Redirect to the URL with only allowed query parameters - String redirectUrl = requestURI + "?" + newQueryString; - response.sendRedirect(redirectUrl); - return false; - } - } - return true; - } - - @Override - public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, - ModelAndView modelAndView) { - } - - @Override - public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, - Exception ex) { - } -} +package stirling.software.SPDF.config; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.Arrays; +import java.util.List; +import java.util.HashMap; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; + +public class CleanUrlInterceptor implements HandlerInterceptor { + + private static final List ALLOWED_PARAMS = Arrays.asList("lang", "endpoint", "endpoints", "logout", "error"); + + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String queryString = request.getQueryString(); + if (queryString != null && !queryString.isEmpty()) { + String requestURI = request.getRequestURI(); + + Map parameters = new HashMap<>(); + + // Keep only the allowed parameters + String[] queryParameters = queryString.split("&"); + for (String param : queryParameters) { + String[] keyValue = param.split("="); + System.out.print("astirli " + keyValue[0]); + if (keyValue.length != 2) { + continue; + } + System.out.print("astirli2 " + keyValue[0]); + + if (ALLOWED_PARAMS.contains(keyValue[0])) { + parameters.put(keyValue[0], keyValue[1]); + } + } + + // If there are any parameters that are not allowed + if (parameters.size() != queryParameters.length) { + // Construct new query string + StringBuilder newQueryString = new StringBuilder(); + for (Map.Entry entry : parameters.entrySet()) { + if (newQueryString.length() > 0) { + newQueryString.append("&"); + } + newQueryString.append(entry.getKey()).append("=").append(entry.getValue()); + } + + // Redirect to the URL with only allowed query parameters + String redirectUrl = requestURI + "?" + newQueryString; + response.sendRedirect(redirectUrl); + return false; + } + } + return true; + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + ModelAndView modelAndView) { + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, + Exception ex) { + } +} diff --git a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java index 5befd511..77191a41 100644 --- a/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java +++ b/src/main/java/stirling/software/SPDF/config/EndpointInterceptor.java @@ -1,26 +1,26 @@ -package stirling.software.SPDF.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -@Component -public class EndpointInterceptor implements HandlerInterceptor { - - @Autowired - private EndpointConfiguration endpointConfiguration; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { - String requestURI = request.getRequestURI(); - if (!endpointConfiguration.isEndpointEnabled(requestURI)) { - response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled"); - return false; - } - return true; - } +package stirling.software.SPDF.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class EndpointInterceptor implements HandlerInterceptor { + + @Autowired + private EndpointConfiguration endpointConfiguration; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + String requestURI = request.getRequestURI(); + if (!endpointConfiguration.isEndpointEnabled(requestURI)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled"); + return false; + } + return true; + } } \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/config/MetricsConfig.java b/src/main/java/stirling/software/SPDF/config/MetricsConfig.java index 25d6d8d6..1cdc99e3 100644 --- a/src/main/java/stirling/software/SPDF/config/MetricsConfig.java +++ b/src/main/java/stirling/software/SPDF/config/MetricsConfig.java @@ -1,24 +1,24 @@ -package stirling.software.SPDF.config; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import io.micrometer.core.instrument.Meter; -import io.micrometer.core.instrument.config.MeterFilter; -import io.micrometer.core.instrument.config.MeterFilterReply; - -@Configuration -public class MetricsConfig { - - @Bean - public MeterFilter meterFilter() { - return new MeterFilter() { - @Override - public MeterFilterReply accept(Meter.Id id) { - if (id.getName().equals("http.requests")) { - return MeterFilterReply.NEUTRAL; - } - return MeterFilterReply.DENY; - } - }; - } +package stirling.software.SPDF.config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.config.MeterFilter; +import io.micrometer.core.instrument.config.MeterFilterReply; + +@Configuration +public class MetricsConfig { + + @Bean + public MeterFilter meterFilter() { + return new MeterFilter() { + @Override + public MeterFilterReply accept(Meter.Id id) { + if (id.getName().equals("http.requests")) { + return MeterFilterReply.NEUTRAL; + } + return MeterFilterReply.DENY; + } + }; + } } \ No newline at end of file diff --git a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java index 326ba09d..6ee59db7 100644 --- a/src/main/java/stirling/software/SPDF/config/MetricsFilter.java +++ b/src/main/java/stirling/software/SPDF/config/MetricsFilter.java @@ -1,48 +1,48 @@ -package stirling.software.SPDF.config; - -import java.io.IOException; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.web.filter.OncePerRequestFilter; - -import io.micrometer.core.instrument.Counter; -import io.micrometer.core.instrument.MeterRegistry; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -@Component -public class MetricsFilter extends OncePerRequestFilter { - - private final MeterRegistry meterRegistry; - - @Autowired - public MetricsFilter(MeterRegistry meterRegistry) { - this.meterRegistry = meterRegistry; - } - - @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) - throws ServletException, IOException { - String uri = request.getRequestURI(); - - //System.out.println("uri="+uri + ", method=" + request.getMethod() ); - // Ignore static resources - if (!(uri.startsWith("/js") || uri.startsWith("api-docs") || uri.endsWith("robots.txt") || uri.startsWith("/images") || uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) { - Counter counter = Counter.builder("http.requests") - .tag("uri", uri) - .tag("method", request.getMethod()) - .register(meterRegistry); - - counter.increment(); - //System.out.println("Counted"); - } - - filterChain.doFilter(request, response); - } - - - -} +package stirling.software.SPDF.config; + +import java.io.IOException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class MetricsFilter extends OncePerRequestFilter { + + private final MeterRegistry meterRegistry; + + @Autowired + public MetricsFilter(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + String uri = request.getRequestURI(); + + //System.out.println("uri="+uri + ", method=" + request.getMethod() ); + // Ignore static resources + if (!(uri.startsWith("/js") || uri.startsWith("api-docs") || uri.endsWith("robots.txt") || uri.startsWith("/images") || uri.endsWith(".png") || uri.endsWith(".ico") || uri.endsWith(".css") || uri.endsWith(".svg")|| uri.endsWith(".js") || uri.contains("swagger") || uri.startsWith("/api"))) { + Counter counter = Counter.builder("http.requests") + .tag("uri", uri) + .tag("method", request.getMethod()) + .register(meterRegistry); + + counter.increment(); + //System.out.println("Counted"); + } + + filterChain.doFilter(request, response); + } + + + +} diff --git a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index d7495aca..573873da 100644 --- a/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -1,36 +1,36 @@ -package stirling.software.SPDF.config; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Properties; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import io.swagger.v3.oas.models.Components; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.info.Info; - -@Configuration -public class OpenApiConfig { - - @Bean - public OpenAPI customOpenAPI() { - String version = getClass().getPackage().getImplementationVersion(); - if (version == null) { - Properties props = new Properties(); - try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) { - props.load(input); - version = props.getProperty("version"); - } catch (IOException ex) { - ex.printStackTrace(); - version = "1.0.0"; // default version if all else fails - } - } - - return new OpenAPI().components(new Components()).info( - new Info().title("Stirling PDF API").version(version).description("API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")); - } - - -} +package stirling.software.SPDF.config; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI customOpenAPI() { + String version = getClass().getPackage().getImplementationVersion(); + if (version == null) { + Properties props = new Properties(); + try (InputStream input = getClass().getClassLoader().getResourceAsStream("version.properties")) { + props.load(input); + version = props.getProperty("version"); + } catch (IOException ex) { + ex.printStackTrace(); + version = "1.0.0"; // default version if all else fails + } + } + + return new OpenAPI().components(new Components()).info( + new Info().title("Stirling PDF API").version(version).description("API documentation for all Server-Side processing.\nPlease note some functionality might be UI only and missing from here.")); + } + + +} diff --git a/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java b/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java index a25cbbbf..28653d35 100644 --- a/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java +++ b/src/main/java/stirling/software/SPDF/config/StartupApplicationListener.java @@ -1,20 +1,20 @@ -package stirling.software.SPDF.config; - - -import org.springframework.context.ApplicationListener; -import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; - -@Component -public class StartupApplicationListener implements ApplicationListener { - - public static LocalDateTime startTime; - - @Override - public void onApplicationEvent(ContextRefreshedEvent event) { - startTime = LocalDateTime.now(); - } -} - +package stirling.software.SPDF.config; + + +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; + +@Component +public class StartupApplicationListener implements ApplicationListener { + + public static LocalDateTime startTime; + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + startTime = LocalDateTime.now(); + } +} + diff --git a/src/main/java/stirling/software/SPDF/config/UserBasedRateLimitingFilter.java b/src/main/java/stirling/software/SPDF/config/UserBasedRateLimitingFilter.java new file mode 100644 index 00000000..441cd080 --- /dev/null +++ b/src/main/java/stirling/software/SPDF/config/UserBasedRateLimitingFilter.java @@ -0,0 +1,68 @@ +package stirling.software.SPDF.config; + +import java.io.IOException; +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket; +import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.Refill; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +@Component +public class UserBasedRateLimitingFilter extends OncePerRequestFilter { + + private final Map buckets = new ConcurrentHashMap<>(); + + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + String method = request.getMethod(); + if (!"POST".equalsIgnoreCase(method)) { + filterChain.doFilter(request, response); + return; + } + + + String identifier; + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication != null && authentication.isAuthenticated()) { + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + identifier = userDetails.getUsername(); + } else { + identifier = request.getRemoteAddr(); // Use IP as identifier if not authenticated + } + + Bucket userBucket = buckets.computeIfAbsent(identifier, k -> createUserBucket()); + + if (userBucket.tryConsume(1)) { + filterChain.doFilter(request, response); + } else { + response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); + response.getWriter().write("Rate limit exceeded."); + return; + } + } +//https://www.baeldung.com/spring-bucket4j + private Bucket createUserBucket() { + Refill refill = Refill.of(3, Duration.ofDays(1)); + Bandwidth limit = Bandwidth.classic(3, refill).withInitialTokens(3); + return Bucket4j.builder().addLimit(limit).build(); + } +} + diff --git a/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java b/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java index 10a88e97..e4b75453 100644 --- a/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java +++ b/src/main/java/stirling/software/SPDF/config/WebMvcConfig.java @@ -1,27 +1,27 @@ -package stirling.software.SPDF.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -public class WebMvcConfig implements WebMvcConfigurer { - - @Autowired - private EndpointInterceptor endpointInterceptor; - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(endpointInterceptor); - } - - @Override - public void addResourceHandlers(ResourceHandlerRegistry registry) { - // Handler for external static resources - registry.addResourceHandler("/**") - .addResourceLocations("file:customFiles/static/", "classpath:/static/") - .setCachePeriod(0); // Optional: disable caching - } -} +package stirling.software.SPDF.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired + private EndpointInterceptor endpointInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(endpointInterceptor); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + // Handler for external static resources + registry.addResourceHandler("/**") + .addResourceLocations("file:customFiles/static/", "classpath:/static/") + .setCachePeriod(0); // Optional: disable caching + } +}