diff --git a/CHANGELOG.md b/CHANGELOG.md index 151c28d9..d5ac35f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +--- +## Version 3.0.13, 4/28/2023 + +### Added + +Added optional static content HTTP-GET request filter in rest.yaml + +### Removed + +N/A + +### Changed + +Updated guava to version 33.1.0-jre + --- ## Version 3.0.12, 4/24/2023 diff --git a/benchmark/benchmark-client/pom.xml b/benchmark/benchmark-client/pom.xml index 99668939..404e4530 100644 --- a/benchmark/benchmark-client/pom.xml +++ b/benchmark/benchmark-client/pom.xml @@ -7,7 +7,7 @@ benchmark-client jar - 3.0.12 + 3.0.13 Benchmark client @@ -45,7 +45,7 @@ org.platformlambda platform-core - 3.0.12 + 3.0.13 diff --git a/benchmark/benchmark-server/pom.xml b/benchmark/benchmark-server/pom.xml index a5735957..64188b82 100644 --- a/benchmark/benchmark-server/pom.xml +++ b/benchmark/benchmark-server/pom.xml @@ -7,7 +7,7 @@ benchmark-server jar - 3.0.12 + 3.0.13 Benchmark server @@ -45,7 +45,7 @@ org.platformlambda platform-core - 3.0.12 + 3.0.13 diff --git a/connectors/adapters/hazelcast/hazelcast-connector/pom.xml b/connectors/adapters/hazelcast/hazelcast-connector/pom.xml index 75f5b569..a0a2f4e0 100644 --- a/connectors/adapters/hazelcast/hazelcast-connector/pom.xml +++ b/connectors/adapters/hazelcast/hazelcast-connector/pom.xml @@ -7,7 +7,7 @@ hazelcast-connector jar - 3.0.12 + 3.0.13 Cloud connector for Hazelcast cluster @@ -42,7 +42,7 @@ org.platformlambda cloud-connector - 3.0.12 + 3.0.13 diff --git a/connectors/adapters/hazelcast/hazelcast-presence/pom.xml b/connectors/adapters/hazelcast/hazelcast-presence/pom.xml index dd4030f4..5d5fcba1 100644 --- a/connectors/adapters/hazelcast/hazelcast-presence/pom.xml +++ b/connectors/adapters/hazelcast/hazelcast-presence/pom.xml @@ -5,7 +5,7 @@ org.platformlambda hazelcast-presence jar - 3.0.12 + 3.0.13 Presence monitor for Hazelcast @@ -40,13 +40,13 @@ org.platformlambda service-monitor - 3.0.12 + 3.0.13 org.platformlambda hazelcast-connector - 3.0.12 + 3.0.13 diff --git a/connectors/adapters/kafka/kafka-connector/pom.xml b/connectors/adapters/kafka/kafka-connector/pom.xml index ef79a779..82df432a 100644 --- a/connectors/adapters/kafka/kafka-connector/pom.xml +++ b/connectors/adapters/kafka/kafka-connector/pom.xml @@ -7,7 +7,7 @@ kafka-connector jar - 3.0.12 + 3.0.13 Cloud connector for Kafka cluster @@ -42,7 +42,7 @@ org.platformlambda cloud-connector - 3.0.12 + 3.0.13 diff --git a/connectors/adapters/kafka/kafka-presence/pom.xml b/connectors/adapters/kafka/kafka-presence/pom.xml index e23886a6..87dbbde8 100644 --- a/connectors/adapters/kafka/kafka-presence/pom.xml +++ b/connectors/adapters/kafka/kafka-presence/pom.xml @@ -5,7 +5,7 @@ org.platformlambda kafka-presence jar - 3.0.12 + 3.0.13 Presence monitor for Kafka @@ -40,13 +40,13 @@ org.platformlambda service-monitor - 3.0.12 + 3.0.13 org.platformlambda kafka-connector - 3.0.12 + 3.0.13 diff --git a/connectors/adapters/kafka/kafka-standalone/pom.xml b/connectors/adapters/kafka/kafka-standalone/pom.xml index d6c612d8..35443039 100644 --- a/connectors/adapters/kafka/kafka-standalone/pom.xml +++ b/connectors/adapters/kafka/kafka-standalone/pom.xml @@ -7,7 +7,7 @@ kafka-standalone jar - 3.0.12 + 3.0.13 Standalone kafka system for dev @@ -42,7 +42,7 @@ org.platformlambda platform-core - 3.0.12 + 3.0.13 diff --git a/connectors/core/cloud-connector/pom.xml b/connectors/core/cloud-connector/pom.xml index 70f4eff6..cfb34296 100644 --- a/connectors/core/cloud-connector/pom.xml +++ b/connectors/core/cloud-connector/pom.xml @@ -7,7 +7,7 @@ cloud-connector jar - 3.0.12 + 3.0.13 Cloud connector module @@ -42,7 +42,7 @@ org.platformlambda platform-core - 3.0.12 + 3.0.13 diff --git a/connectors/core/service-monitor/pom.xml b/connectors/core/service-monitor/pom.xml index 192522b1..729d0858 100644 --- a/connectors/core/service-monitor/pom.xml +++ b/connectors/core/service-monitor/pom.xml @@ -5,7 +5,7 @@ org.platformlambda service-monitor jar - 3.0.12 + 3.0.13 Presence monitor module @@ -40,13 +40,13 @@ org.platformlambda cloud-connector - 3.0.12 + 3.0.13 org.platformlambda platform-core - 3.0.12 + 3.0.13 diff --git a/examples/lambda-example/pom.xml b/examples/lambda-example/pom.xml index 2dc4704b..e0895c1e 100644 --- a/examples/lambda-example/pom.xml +++ b/examples/lambda-example/pom.xml @@ -7,7 +7,7 @@ lambda-example jar - 3.0.12 + 3.0.13 Composable application example @@ -42,7 +42,7 @@ org.platformlambda platform-core - 3.0.12 + 3.0.13 diff --git a/examples/rest-spring-2-example/pom.xml b/examples/rest-spring-2-example/pom.xml index 89f7e37c..d79dd1b1 100644 --- a/examples/rest-spring-2-example/pom.xml +++ b/examples/rest-spring-2-example/pom.xml @@ -5,7 +5,7 @@ com.accenture rest-spring-2-example - 3.0.12 + 3.0.13 jar Spring Boot 2 example @@ -42,7 +42,7 @@ org.platformlambda rest-spring-2 - 3.0.12 + 3.0.13 diff --git a/examples/rest-spring-3-example/pom.xml b/examples/rest-spring-3-example/pom.xml index e2a724fe..674f7181 100644 --- a/examples/rest-spring-3-example/pom.xml +++ b/examples/rest-spring-3-example/pom.xml @@ -5,7 +5,7 @@ com.accenture rest-spring-3-example - 3.0.12 + 3.0.13 jar Spring Boot 3 example @@ -41,7 +41,7 @@ org.platformlambda rest-spring-3 - 3.0.12 + 3.0.13 diff --git a/extensions/api-playground/pom.xml b/extensions/api-playground/pom.xml index ca884cbd..c047cb3e 100644 --- a/extensions/api-playground/pom.xml +++ b/extensions/api-playground/pom.xml @@ -7,7 +7,7 @@ api-playground jar - 3.0.12 + 3.0.13 API playground using OpenAPI @@ -43,7 +43,7 @@ org.platformlambda rest-spring-2 - 3.0.12 + 3.0.13 diff --git a/extensions/simple-scheduler/pom.xml b/extensions/simple-scheduler/pom.xml index 80471dfb..2bf920b2 100644 --- a/extensions/simple-scheduler/pom.xml +++ b/extensions/simple-scheduler/pom.xml @@ -7,7 +7,7 @@ simple-scheduler jar - 3.0.12 + 3.0.13 Simple Scheduler @@ -43,7 +43,7 @@ org.platformlambda rest-spring-2 - 3.0.12 + 3.0.13 diff --git a/guides/CHAPTER-3.md b/guides/CHAPTER-3.md index c13e1db8..62181928 100644 --- a/guides/CHAPTER-3.md +++ b/guides/CHAPTER-3.md @@ -212,6 +212,64 @@ headers: - "Pragma: no-cache" - "Expires: Thu, 01 Jan 1970 00:00:00 GMT" ``` + +## Static content + +Static content (HTML/CSS/JS bundle), if any, can be placed in the "resources/public" folder in your +application project root. It is because the default value for the "static.html.folder" parameter +in the application configuration is "classpath:/resources/public". If you want to place your +static content elsewhere, you may adjust this parameter. You may point it to the local file system +such as "file:/tmp/html". + +For security reason, you may add the following configuration in the rest.yaml. +The following example is shown in the unit test section of the platform-core library module. + +```yaml +# +# Optional HTTP GET request filter for static HTML/CSS/JS files +# ------------------------------------------------------------- +# +# This provides a programmatic way to protect certain static content. +# +# The filter can be used to inspect HTTP path, headers and parameters. +# The typical use case is to check cookies and perform browser redirection +# for SSO login. Another use case is to selectively add security HTTP +# response headers such as cache control and X-Frame-Options. +# +# In the following example, the filter applies to all static content +# HTTP-GET requests except those with the file extension ".css". +# You can implement a function with the service route "http.request.filter". +# The input to the function will be an AsyncHttpRequest object. +# +static-content-filter: + path: ["/"] + excludes: [".css"] + service: "http.request.filter" +``` + +The sample request filter function is available in the platform-core project like this: + +```java +@PreLoad(route="http.request.filter", instances=100) +public class GetRequestFilter implements LambdaFunction { + + @Override + public Object handleEvent(Map headers, Object input, int instance) throws Exception { + return new EventEnvelope().setHeader("x-filter", "demo"); + } +} +``` + +In the above http.request.filter, it adds a HTTP response header "X-Filter" for the unit test +to validate. + +If you set status code in the return EventEnvelope to 302 and add a header "Location", the system +will redirect the browser to the given URL in the location header. Please be careful to avoid +HTTP redirection loop. + +Similarly, you can throw exception and the HTTP request will be rejected with the given status +code and error message accordingly. +
| Chapter-2 | Home | Chapter-4 | diff --git a/pom.xml b/pom.xml index 0c81dc3e..56425086 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.accenture.mercury parent-mercury pom - 3.0.12 + 3.0.13 Parent Mercury diff --git a/system/platform-core/pom.xml b/system/platform-core/pom.xml index f9d4e4ca..e5f74591 100644 --- a/system/platform-core/pom.xml +++ b/system/platform-core/pom.xml @@ -6,7 +6,7 @@ org.platformlambda platform-core jar - 3.0.12 + 3.0.13 Mercury platform-core module @@ -85,7 +85,7 @@ com.google.guava guava - 33.0.0-jre + 33.1.0-jre diff --git a/system/platform-core/src/main/java/org/platformlambda/automation/config/RoutingEntry.java b/system/platform-core/src/main/java/org/platformlambda/automation/config/RoutingEntry.java index b01535b2..4626dc55 100644 --- a/system/platform-core/src/main/java/org/platformlambda/automation/config/RoutingEntry.java +++ b/system/platform-core/src/main/java/org/platformlambda/automation/config/RoutingEntry.java @@ -18,10 +18,7 @@ package org.platformlambda.automation.config; -import org.platformlambda.automation.models.AssignedRoute; -import org.platformlambda.automation.models.CorsInfo; -import org.platformlambda.automation.models.HeaderInfo; -import org.platformlambda.automation.models.RouteInfo; +import org.platformlambda.automation.models.*; import org.platformlambda.core.system.AppStarter; import org.platformlambda.core.util.ConfigReader; import org.platformlambda.core.util.Utility; @@ -77,6 +74,7 @@ public class RoutingEntry { private static final Map requestHeaderInfo = new HashMap<>(); private static final Map responseHeaderInfo = new HashMap<>(); private static final List urlPaths = new ArrayList<>(); + private static SimpleHttpFilter requestFilter; private static final RoutingEntry instance = new RoutingEntry(); private RoutingEntry() { @@ -87,6 +85,10 @@ public static RoutingEntry getInstance() { return instance; } + public SimpleHttpFilter getRequestFilter() { + return requestFilter; + } + public AssignedRoute getRouteInfo(String method, String url) { Utility util = Utility.getInstance(); StringBuilder sb = new StringBuilder(); @@ -192,8 +194,39 @@ private boolean matchRoute(List urlParts, List segments, boolean return true; } - @SuppressWarnings("unchecked") + @SuppressWarnings(value="unchecked") + private SimpleHttpFilter getFilter(ConfigReader config) { + Object path = config.get("static-content-filter.path"); + Object exclusion = config.get("static-content-filter.excludes"); + String service = config.getProperty("static-content-filter.service"); + if (path instanceof List && exclusion instanceof List && service != null && !service.isEmpty()) { + if (Utility.getInstance().validServiceName(service)) { + log.info("Static content HTTP-GET filter installed: {} -> {}, excludes extensions {}", + path, service, exclusion); + List pathList = new ArrayList<>(); + List excludeExtensions = new ArrayList<>(); + List pList = (List) path; + List eList = (List) exclusion; + for (int i=0; i < pList.size(); i++) { + pathList.add(config.getProperty("static-content-filter.path["+i+"]")); + } + for (int i=0; i < eList.size(); i++) { + excludeExtensions.add(config.getProperty("static-content-filter.excludes["+i+"]")); + } + if (pathList.isEmpty()) { + log.error("Static content HTTP-GET filter {} ignored: - path is empty", service); + } + return new SimpleHttpFilter(pathList, excludeExtensions, service); + } else { + log.error("Static content HTTP-GET filter ignored: '{} -> {}' - invalid service name", path, service); + } + } + return null; + } + + @SuppressWarnings(value = "unchecked") public void load(ConfigReader config) { + requestFilter = getFilter(config); if (config.exists(HEADERS)) { Object headerList = config.get(HEADERS); boolean valid = false; @@ -569,7 +602,7 @@ private String getUrl(String url, boolean exact) { private boolean validArgument(String arg) { if (arg.startsWith("{") && arg.endsWith("}")) { String v = arg.substring(1, arg.length()-1); - if (v.length() == 0) { + if (v.isEmpty()) { return false; } else { return !v.contains("{") && !v.contains("}"); @@ -643,7 +676,7 @@ private boolean validCorsElement(String element) { return false; } String value = element.substring(colon+1).trim(); - if (value.length() == 0) { + if (value.isEmpty()) { log.error("Missing value in cors header {}", element.substring(0, colon)); return false; } diff --git a/system/platform-core/src/main/java/org/platformlambda/automation/models/SimpleHttpFilter.java b/system/platform-core/src/main/java/org/platformlambda/automation/models/SimpleHttpFilter.java new file mode 100644 index 00000000..d570505e --- /dev/null +++ b/system/platform-core/src/main/java/org/platformlambda/automation/models/SimpleHttpFilter.java @@ -0,0 +1,34 @@ +/* + + Copyright 2018-2024 Accenture Technology + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +package org.platformlambda.automation.models; + +import java.util.List; + +public class SimpleHttpFilter { + + public List pathList; + public List excludeExtensions; + public String service; + + public SimpleHttpFilter(List pathList, List excludeExtensions, String service) { + this.pathList = pathList; + this.excludeExtensions = excludeExtensions; + this.service = service; + } +} diff --git a/system/platform-core/src/main/java/org/platformlambda/automation/services/ServiceGateway.java b/system/platform-core/src/main/java/org/platformlambda/automation/services/ServiceGateway.java index 57970e12..40a2305e 100644 --- a/system/platform-core/src/main/java/org/platformlambda/automation/services/ServiceGateway.java +++ b/system/platform-core/src/main/java/org/platformlambda/automation/services/ServiceGateway.java @@ -31,10 +31,7 @@ import org.platformlambda.core.models.EventEnvelope; import org.platformlambda.core.serializers.SimpleMapper; import org.platformlambda.core.serializers.SimpleXmlParser; -import org.platformlambda.core.system.AppStarter; -import org.platformlambda.core.system.EventEmitter; -import org.platformlambda.core.system.ObjectStreamWriter; -import org.platformlambda.core.system.Platform; +import org.platformlambda.core.system.*; import org.platformlambda.core.util.AppConfigReader; import org.platformlambda.core.util.ConfigReader; import org.platformlambda.core.util.CryptoApi; @@ -82,6 +79,7 @@ public class ServiceGateway { private static final String ETAG = "ETag"; private static final String IF_NONE_MATCH = "If-None-Match"; private static final int BUFFER_SIZE = 4 * 1024; + private static final long FILTER_TIMEOUT = 10000; // requestId -> context private static final ConcurrentMap contexts = new ConcurrentHashMap<>(); private static final Map mimeTypes = new HashMap<>(); @@ -139,7 +137,7 @@ public static void initialize() { mimeTypes.put(kv.getKey().toLowerCase(), kv.getValue().toString().toLowerCase()); } } - if (mimeTypes.size() > 0) { + if (!mimeTypes.isEmpty()) { log.info("Loaded {} mime types", mimeTypes.size()); } // register authentication handler @@ -167,25 +165,62 @@ public void handleEvent(AssignedRoute route, String requestId, int status, Strin AsyncContextHolder holder = contexts.get(requestId); if (holder != null) { HttpServerRequest request = holder.request; + String path = Utility.getInstance().getUrlDecodedPath(request.path()); SimpleHttpUtility httpUtil = SimpleHttpUtility.getInstance(); + HttpServerResponse response = request.response(); if (error != null) { if (GET.equals(request.method().name()) && status == 404) { - String path = Utility.getInstance().getUrlDecodedPath(request.path()); EtagFile file = getStaticFile(path); if (file != null) { - HttpServerResponse response = request.response(); - response.putHeader(CONTENT_TYPE, getFileContentType(file.name)); - String ifNoneMatch = request.getHeader(IF_NONE_MATCH); - if (file.sameTag(ifNoneMatch)) { - response.setStatusCode(304); - response.putHeader(CONTENT_LEN, "0"); - } else { - response.putHeader(ETAG, file.eTag); - response.putHeader(CONTENT_LEN, String.valueOf(file.content.length)); - response.write(Buffer.buffer(file.content)); + SimpleHttpFilter filter = RoutingEntry.getInstance().getRequestFilter(); + if (filter != null && GET.equals(request.method().name()) && needFilter(filter, path)) { + EventEmitter po = EventEmitter.getInstance(); + if (po.exists(filter.service)) { + try { + EventEnvelope event = new EventEnvelope().setTo(filter.service) + .setBody(getHttpRequestHeaders(request, path)); + po.asyncRequest(event, FILTER_TIMEOUT, false) + .onSuccess(filtered -> { + Utility util = Utility.getInstance(); + // this allows the filter to set HTTP response headers + Map headers = filtered.getHeaders(); + headers.forEach(response::putHeader); + // this allows the filter to send redirect-url or throw exception + if (filtered.getStatus() == 200) { + sendStaticFile(requestId, file, request, response); + } else { + response.setStatusCode(filtered.getStatus()); + final byte[] content; + Object body = filtered.getRawBody(); + if (body == null) { + content = null; + } else if (body instanceof String) { + content = util.getUTF((String) body); + } else if (body instanceof byte[]) { + content = (byte[]) body; + } else if (body instanceof Map) { + content = SimpleMapper.getInstance().getMapper().writeValueAsBytes(body); + } else { + content = util.getUTF(body.toString()); + } + if (content != null) { + response.write(Buffer.buffer(content)); + } + closeContext(requestId); + response.end(); + } + }); + return; + } catch (IOException e) { + log.error("Unable to filter static content HTTP-GET {} - {}", + filter.service, e.getMessage()); + } + } else { + log.warn("Static content HTTP-GET filter {} ignored because it does not exist", + filter.service); + } } - closeContext(requestId); - response.end(); + sendStaticFile(requestId, file, request, response); return; } } @@ -200,6 +235,79 @@ public void handleEvent(AssignedRoute route, String requestId, int status, Strin } } + private boolean needFilter(SimpleHttpFilter filter, String path) { + boolean pathFound = false; + for (String p: filter.pathList) { + if (path.startsWith(p)) { + pathFound = true; + break; + } + } + boolean exclusionFound = false; + for (String e: filter.excludeExtensions) { + if (path.endsWith(e)) { + exclusionFound = true; + break; + } + } + return pathFound && !exclusionFound; + } + + private AsyncHttpRequest getHttpRequestHeaders(HttpServerRequest request, String path) { + AsyncHttpRequest req = new AsyncHttpRequest(); + String queryString = request.query(); + if (queryString != null) { + req.setQueryString(queryString); + } + req.setUrl(path); + req.setMethod(request.method().name()); + req.setSecure(HTTPS.equals(request.getHeader(PROTOCOL))); + MultiMap params = request.params(); + for (String key: params.names()) { + List values = params.getAll(key); + if (values.size() == 1) { + req.setQueryParameter(key, values.get(0)); + } + if (values.size() > 1) { + req.setQueryParameter(key, values); + } + } + boolean hasCookies = false; + MultiMap headerMap = request.headers(); + for (String key: headerMap.names()) { + String value = headerMap.get(key); + if (COOKIE.equalsIgnoreCase(key)) { + hasCookies = true; + } else { + req.setHeader(key, value); + } + } + // load cookies + if (hasCookies) { + Set cookies = request.cookies(); + for (Cookie c : cookies) { + req.setCookie(c.getName(), c.getValue()); + } + } + return req.setRemoteIp(request.remoteAddress().hostAddress()); + } + + private void sendStaticFile(String requestId, EtagFile file, + HttpServerRequest request, HttpServerResponse response) { + response.putHeader(CONTENT_TYPE, getFileContentType(file.name)); + String ifNoneMatch = request.getHeader(IF_NONE_MATCH); + if (file.sameTag(ifNoneMatch)) { + response.setStatusCode(304); + response.putHeader(CONTENT_LEN, "0"); + } else { + response.putHeader(ETAG, file.eTag); + response.putHeader(CONTENT_LEN, String.valueOf(file.content.length)); + response.write(Buffer.buffer(file.content)); + } + closeContext(requestId); + response.end(); + } + /** * This is a very primitive way to resolve content-type for proper loading of * HTML, CSS and Javascript contents by a browser. @@ -386,9 +494,7 @@ private void routeRequest(String requestId, AssignedRoute route, AsyncContextHol Map headers = new HashMap<>(); MultiMap headerMap = request.headers(); for (String key: headerMap.names()) { - /* - * Single-value HTTP header is assumed. - */ + // Single-value HTTP header is assumed String value = headerMap.get(key); if (COOKIE.equalsIgnoreCase(key)) { hasCookies = true; @@ -470,7 +576,7 @@ private void routeRequest(String requestId, AssignedRoute route, AsyncContextHol String text = util.getUTF(requestBody.toByteArray()); String trimmed = text.trim(); try { - if (trimmed.length() == 0) { + if (trimmed.isEmpty()) { req.setBody(new HashMap<>()); } else if (trimmed.startsWith("{") && trimmed.endsWith("}")) { req.setBody(SimpleMapper.getInstance().getMapper().readValue(text, Map.class)); diff --git a/system/platform-core/src/main/java/org/platformlambda/core/system/EventEmitter.java b/system/platform-core/src/main/java/org/platformlambda/core/system/EventEmitter.java index e83af102..44069d56 100644 --- a/system/platform-core/src/main/java/org/platformlambda/core/system/EventEmitter.java +++ b/system/platform-core/src/main/java/org/platformlambda/core/system/EventEmitter.java @@ -595,7 +595,7 @@ public List getAllFutureEvents() { * @return event ID list */ public List getFutureEvents(String to) { - if (to == null || to.length() == 0) { + if (to == null || to.isEmpty()) { throw new IllegalArgumentException("Missing 'to'"); } List result = new ArrayList<>(); @@ -887,7 +887,7 @@ private String getTargetFromUrl(URI url) { throw new IllegalArgumentException(HTTP_OR_HTTPS); } String host = url.getHost().trim(); - if (host.length() == 0) { + if (host.isEmpty()) { throw new IllegalArgumentException("Unable to resolve target host as domain or IP address"); } int port = url.getPort(); diff --git a/system/platform-core/src/test/java/org/platformlambda/automation/RestEndpointTest.java b/system/platform-core/src/test/java/org/platformlambda/automation/RestEndpointTest.java index acc2a1c6..ff71e9c1 100644 --- a/system/platform-core/src/test/java/org/platformlambda/automation/RestEndpointTest.java +++ b/system/platform-core/src/test/java/org/platformlambda/automation/RestEndpointTest.java @@ -96,7 +96,7 @@ public void optionsMethodTest() throws IOException, InterruptedException { Assert.assertEquals("*", response.getHeader("access-control-Allow-Origin")); } - @SuppressWarnings("unchecked") + @SuppressWarnings(value = "unchecked") @Test public void serviceTest() throws IOException, InterruptedException { final BlockingQueue bench = new ArrayBlockingQueue<>(1); @@ -126,6 +126,8 @@ public void serviceTest() throws IOException, InterruptedException { Assert.assertEquals(10, map.getElement("timeout")); Assert.assertEquals("y", map.getElement("parameters.query.x1")); Assert.assertEquals(list, map.getElement("parameters.query.x2")); + // the HTTP request filter will not execute because /api path is excluded + Assert.assertNull(response.getHeader("x-filter")); } @Test @@ -856,7 +858,7 @@ public void getIndexWithoutExtension() throws IOException, InterruptedException EventEmitter po = EventEmitter.getInstance(); AsyncHttpRequest req = new AsyncHttpRequest(); req.setMethod("GET"); - req.setUrl("/index"); + req.setUrl("/"); req.setTargetHost("http://127.0.0.1:"+port); EventEnvelope request = new EventEnvelope().setTo(HTTP_REQUEST).setBody(req); Future res = po.asyncRequest(request, RPC_TIMEOUT); @@ -869,6 +871,8 @@ public void getIndexWithoutExtension() throws IOException, InterruptedException InputStream in = this.getClass().getResourceAsStream("/public/index.html"); String content = util.stream2str(in); Assert.assertEquals(content, html); + // the HTTP request filter will add a test header + Assert.assertEquals("demo", response.getHeader("x-filter")); } @Test @@ -891,6 +895,8 @@ public void getCssPage() throws IOException, InterruptedException { InputStream in = this.getClass().getResourceAsStream("/public/sample.css"); String content = util.stream2str(in); Assert.assertEquals(content, html); + // the HTTP request filter is not executed because ".css" extension is excluded in rest.yaml + Assert.assertNull(response.getHeader("x-filter")); } @Test @@ -913,6 +919,8 @@ public void getJsPage() throws IOException, InterruptedException { InputStream in = this.getClass().getResourceAsStream("/public/sample.js"); String content = util.stream2str(in); Assert.assertEquals(content, html); + // the HTTP request filter will add a test header + Assert.assertEquals("demo", response.getHeader("x-filter")); } @Test diff --git a/system/platform-core/src/test/java/org/platformlambda/automation/service/GetRequestFilter.java b/system/platform-core/src/test/java/org/platformlambda/automation/service/GetRequestFilter.java new file mode 100644 index 00000000..90de1b69 --- /dev/null +++ b/system/platform-core/src/test/java/org/platformlambda/automation/service/GetRequestFilter.java @@ -0,0 +1,34 @@ +/* + + Copyright 2018-2024 Accenture Technology + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + */ + +package org.platformlambda.automation.service; + +import org.platformlambda.core.annotations.PreLoad; +import org.platformlambda.core.models.EventEnvelope; +import org.platformlambda.core.models.LambdaFunction; + +import java.util.Map; + +@PreLoad(route="http.request.filter", instances=100) +public class GetRequestFilter implements LambdaFunction { + + @Override + public Object handleEvent(Map headers, Object input, int instance) throws Exception { + return new EventEnvelope().setHeader("x-filter", "demo"); + } +} diff --git a/system/platform-core/src/test/resources/rest.yaml b/system/platform-core/src/test/resources/rest.yaml index 04d3cf4f..b9140587 100644 --- a/system/platform-core/src/test/resources/rest.yaml +++ b/system/platform-core/src/test/resources/rest.yaml @@ -107,12 +107,33 @@ rest: headers: header_1 # +# Optional HTTP GET request filter for static HTML/CSS/JS files +# ------------------------------------------------------------- +# +# This provides a programmatic way to protect certain static content. +# +# The filter can be used to inspect HTTP path, headers and parameters. +# The typical use case is to check cookies and perform browser redirection +# for SSO login. Another use case is to selectively add security HTTP +# response headers such as cache control and X-Frame-Options. +# +# In the following example, the filter applies to all static content +# HTTP-GET requests except those with the file extension ".css". +# You can implement a function with the service route "http.request.filter". +# The input to the function will be an AsyncHttpRequest object. +# +static-content-filter: + path: ["/"] + excludes: [".css"] + service: "http.request.filter" +# # CORS HEADERS for pre-flight (HTTP OPTIONS) and normal responses # # Access-Control-Allow-Origin must be "*" or domain name starting with "http://" or "https://" # The use of wildcard "*" should only be allowed for non-prod environments. # -# For production, please add the "api.origin" key in application.properties. +# In the following example, we use the "api.origin" key in application.properties that +# contains the domain name or an environment variable. # cors: - id: cors_1 diff --git a/system/rest-spring-2/pom.xml b/system/rest-spring-2/pom.xml index e05d6d60..ca46f688 100644 --- a/system/rest-spring-2/pom.xml +++ b/system/rest-spring-2/pom.xml @@ -6,7 +6,7 @@ org.platformlambda rest-spring-2 - 3.0.12 + 3.0.13 jar Pre-configured Spring Boot 2 module @@ -45,7 +45,7 @@ org.platformlambda platform-core - 3.0.12 + 3.0.13 org.springframework.boot diff --git a/system/rest-spring-3/pom.xml b/system/rest-spring-3/pom.xml index d8cfba2c..12e16915 100644 --- a/system/rest-spring-3/pom.xml +++ b/system/rest-spring-3/pom.xml @@ -5,7 +5,7 @@ org.platformlambda rest-spring-3 - 3.0.12 + 3.0.13 jar Pre-configured Spring Boot 3 module @@ -43,7 +43,7 @@ org.platformlambda platform-core - 3.0.12 + 3.0.13