diff --git a/README.md b/README.md
index 63d6d37a54..812fe9d74a 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
[![GitHub release](https://img.shields.io/github/release/membrane/service-proxy.svg)](https://github.com/membrane/service-proxy/releases/latest)
[![Hex.pm](https://img.shields.io/hexpm/l/plug.svg)](https://raw.githubusercontent.com/membrane/service-proxy/master/distribution/router/LICENSE.txt)
-A versatile **API Gateway** for **REST**, **WebSockets**, and **legacy Web Services**, built in Java.
+A versatile and lightweight **API Gateway** for **REST** and **legacy SOAP Web Services**, built in Java.
## Features
@@ -15,9 +15,9 @@ A versatile **API Gateway** for **REST**, **WebSockets**, and **legacy Web Servi
- Validate requests and responses against [OpenAPI](distribution/examples/openapi/validation-simple) and **JSON Schema**.
### **API Security**
-- Support for [JSON Web Tokens](#json-web-tokens), [OAuth2](https://www.membrane-soa.org/service-proxy/oauth2-provider-client.html), [API Keys](distribution/examples/api-management), [NTLM](distribution/examples/ntlm), and [Basic Authentication](https://www.membrane-soa.org/api-gateway-doc/current/configuration/reference/basicAuthentication.htm).
+- Support for [JSON Web Tokens](#json-web-tokens), [OAuth2](https://www.membrane-soa.org/service-proxy/oauth2-provider-client.html), [API Keys](#API-Keys), [NTLM](distribution/examples/ntlm), and [Basic Authentication](https://www.membrane-soa.org/api-gateway-doc/current/configuration/reference/basicAuthentication.htm).
- Built-in [OAuth2 Authorization Server](https://www.membrane-soa.org/service-proxy-doc/4.8/security/oauth2/flows/code/index.html).
-- Implement **rate limiting** to control traffic ([example](#rate-limiting)).
+- [Rate limiting](#rate-limiting) and traffic control
- Protection for **GraphQL**, **JSON**, and **XML** APIs against malicious inputs.
### **Legacy Web Services**
@@ -30,6 +30,17 @@ A versatile **API Gateway** for **REST**, **WebSockets**, and **legacy Web Servi
- Flexible [message transformation](#message-transformation) for seamless data processing.
- Embeddable reverse proxy HTTP framework to build custom API gateways.
+# Content
+
+- [Getting Started](#Getting-Started)
+- [Basics](#Basics) Routing, rewriting
+- [Scripting](#scripting)
+- [Message Transformation](#message-transformation)
+- [Security](#security)
+- [Traffic Control](#Traffic-Control) Rate limiting, Load balancing
+- [Legacy Web Services](#soap-web-services) SOAP and WSDL
+- [Operation](#Operation)
+
# Getting Started
## Java
@@ -101,7 +112,7 @@ For detailed Docker setup instructions, see the [Membrane Deployment Guide](http
### Read the Documentation
- For detailed guidance, visit the [official documentation](https://www.membrane-soa.org/service-proxy-doc/).
-# Configuration
+# Basics
### Customizing Membrane
To configure Membrane, edit the `proxies.xml` file located in the `conf` folder.
@@ -212,10 +223,6 @@ The configuration below demonstrates several routing rules, with comments explai
For more routing options, see the [Membrane API documentation](https://www.membrane-api.io/docs/current/api.html).
----
-
-This version adds structure, clear explanations for each rule, and practical use cases for better readability and understanding.
-
### Short Circuit
Sometimes, you may need an endpoint that doesn’t forward requests to a backend. Membrane makes it easy to create such endpoints.
@@ -255,9 +262,43 @@ You can block specific paths (e.g., `/nothing`) while allowing other calls to pa
```
-## Scripting
+### URL Rewriting
+
+The URLs of request can be rewritten dynamically before forwarding them to the backend. This is useful for restructuring API paths or managing legacy endpoints.
+
+#### Example
+The following configuration rewrites requests starting with `/fruitshop` to `/shop/v2`, preserving the remainder of the path:
+
+```xml
+
+ /fruitshop
+
+
+
+
+
+```
+
+#### Testing
+A request to:
+```
+http://localhost:2000/fruitshop/products/4
+```
+will be rewritten to and forwarded to the backend at:
+```
+https://api.predic8.de/shop/v2/products/4
+```
+
+# Scripting
+
+Membrane has powerful scripting features that allow to modify the desired of an API using Groovy or Javascript.
-Membrane has powerful scripting features that allow to realize the desired behaviour of an API. You can use the Groovy or the Javascript language to write small plugins.
+#### Use Cases
+
+- **Custom Responses**: Tailor responses dynamically based on client requests or internal logic.
+- **Mocking APIs**: Simulate API behavior during testing or development phases.
+- **Dynamic Headers**: Add headers conditionally based on business rules.
+- **Debugging**: Inspect incoming requests during development.
### Groovy Scripts
@@ -265,11 +306,11 @@ The following API executes a Groovy script during the request and the response.
```xml
-
- println "I'am executed in the ${flow} flow"
- println "HTTP Headers:\n${header}"
-
-
+
+ println "I'am executed in the ${flow} flow"
+ println "HTTP Headers:\n${header}"
+
+
```
@@ -288,6 +329,121 @@ Content-Length: 390
Content-Type: application/json
```
+#### Dynamically Route to random Target
+
+You can realize a load balancer by setting the destination randomly.
+
+```xml
+
+
+
+ sites = ["https://api.predic8.de","https://membrane-api.io","https://predic8.de"]
+ Collections.shuffle sites
+ exchange.setDestinations(sites)
+
+
+
+
+```
+
+### Create a Response with Groovy
+
+The `groovy` plugin in Membrane allows you to dynamically generate custom responses. The result of the last line of the Groovy script is passed to the plugin. If the result is a `Response` object, it will be returned to the caller.
+
+#### Example
+The following example creates a custom JSON response with a status code of `200`, a specific content type, and a custom header:
+
+```xml
+
+
+ Response.ok()
+ .contentType("application/json")
+ .header("X-Foo", "bar")
+ .body("""
+ {
+ "success": true
+ }
+ """)
+ .build()
+
+
+```
+
+#### How It Works
+- The `Response.ok()` method initializes a new HTTP response with a status of `200 OK`.
+- The `contentType()` method sets the `Content-Type` header, ensuring the response is identified as JSON.
+- The `header()` method adds custom headers to the response.
+- The `body()` method specifies the response payload.
+- The `build()` method finalizes the response object, which is then returned by the `groovy` plugin.
+
+#### Resulting Response
+When accessing this API, the response will look like this:
+
+```
+HTTP/1.1 200 OK
+Content-Type: application/json
+X-Foo: bar
+
+{
+ "success": true
+}
+```
+
+#### Learn More about the Groovy Plugin
+For more information about using Groovy with Membrane, refer to:
+
+- [Groovy Plugin Reference](https://www.membrane-api.io/docs/current/groovy.html).
+- [Sample Project](distribution/examples/groovy)
+
+### JavaScript Extension
+
+In addition to Groovy, Membrane supports JavaScript for implementing custom behavior. This allows you to inspect, modify, or log details about requests and responses.
+
+#### Example
+The following example logs all HTTP headers from incoming requests and responses to the console:
+
+```xml
+
+
+ console.log("------------ Headers: -------------");
+
+ var fields = header.getAllHeaderFields();
+ for (var i = 0; i < fields.length; i++) {
+ console.log(fields[i]);
+ }
+
+ CONTINUE;
+
+
+
+```
+
+The `CONTINUE` keyword ensures that the request continues processing and is forwarded to the target URL.
+
+#### Learn More
+For more details about using JavaScript with Membrane, check the [JavaScript Plugin documentation](https://www.membrane-api.io/docs/current/javascript.html).
+
+### Javascript Extenstion
+
+Besides Groovy you can realize custom behavior with Javascript.
+
+```xml
+
+
+ console.log("------------ Headers: -------------")
+
+ var fields = header.getAllHeaderFields();
+
+ for(i=0;i < fields.length;i++) {
+ console.log(fields[i]);
+ }
+ CONTINUE
+
+
+
+```
+
+
## Message Transformation
### Manipulating HTTP Headers
@@ -586,62 +742,75 @@ Membrane offers lots of security features to protect backend servers.
## API Keys
-Secure any API using a simple API key configuration like this:
+You can define APIs keys directly in your configuration, and Membrane will validate incoming requests against them.
+
+### Example Configuration
+The following configuration secures the `Fruitshop API` by validating a key provided as a query parameter:
```xml
+
-
+
+
+
-
+
+
+
- Hidden API
-
+
-```
+```
+
+### Testing the Configuration
+To test the configuration, pass a valid API key in the query string:
+
+```bash
+curl "http://localhost:2000/shop/v2/products/4?api-key=abc123"
+```
+
+If the key is invalid or missing, Membrane denies access and returns an error response (HTTP 401 Unauthorized).
-This will fetch the API key from the "X-Api-Key" header if present.
-On incorrect key entry or missing key, access is denied and an error response is sent.
-For more complex configurations using RBAC and file-based key stores see: [API Key Plugin Examples](./distribution/examples/security/api-key/rbac/README.md)
+### Advanced Use Cases
+For more complex setups, such as API keys in the HTTP header, role-based access control (RBAC) or file-based key storage, see the [API Key Plugin Examples](./distribution/examples/security/api-key/rbac/README.md).
## JSON Web Tokens
The API below only allows requests with valid tokens from Microsoft's Azure AD. You can also use the JWT validator for other identity providers.
```xml
-
-
-
-
-
+
+
+
+
```
## OAuth2
-### Secure an API with OAuth2
+### Secure APIs with OAuth2
Use OAuth2/OpenID to secure endpoints against Google, Azure AD, GitHub, Keycloak or Membrane authentication servers.
```xml
-
-
-
-
-
- // Get email from OAuth2 and forward it to the backend
- def oauth2 = exc.properties.oauth2
- header.setValue('X-EMAIL',oauth2.userinfo.email)
- CONTINUE
-
-
+
+
+
+
+ // Get email from OAuth2 and forward it to the backend
+ def oauth2 = exc.properties.oauth2
+ header.setValue('X-EMAIL',oauth2.userinfo.email)
+ CONTINUE
+
+
```
@@ -654,19 +823,19 @@ Operate your own identity provider:
```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
```
@@ -675,12 +844,12 @@ See the [OAuth2 Authorization Server](https://www.membrane-soa.org/service-proxy
## Basic Authentication
```xml
-
-
-
-
-
+
+
+
+
+
```
@@ -689,9 +858,8 @@ See the [OAuth2 Authorization Server](https://www.membrane-soa.org/service-proxy
Route to SSL/TLS secured endpoints:
```xml
-
-
+
```
@@ -708,6 +876,8 @@ Secure endpoints with SSL/TLS:
```
+# Traffic Control
+
## Rate Limiting
Limit the number of incoming requests:
@@ -720,7 +890,7 @@ Limit the number of incoming requests:
```
-# Load balancing
+## Load balancing
Distribute workload to multiple backend nodes. [See the example](distribution/examples/loadbalancing)
@@ -739,18 +909,6 @@ Distribute workload to multiple backend nodes. [See the example](distribution/ex
```
-# Rewrite URLs
-
-```xml
-
-
-
-
-
-
-
-```
-
# Log HTTP
Log data about requests and responses to a file or [database](distribution/examples/logging/jdbc-database) as [CSV](distribution/examples/logging/csv)
diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/groovy/GroovyInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/groovy/GroovyInterceptor.java
index b472ca73a9..780ed75caf 100644
--- a/core/src/main/java/com/predic8/membrane/core/interceptor/groovy/GroovyInterceptor.java
+++ b/core/src/main/java/com/predic8/membrane/core/interceptor/groovy/GroovyInterceptor.java
@@ -46,12 +46,12 @@ protected void initInternal() {
try {
script = new GroovyLanguageSupport().compileScript(router.getBackgroundInitializator(), null, src);
} catch (MultipleCompilationErrorsException e) {
- logGroovyException(e);
+ logScriptExceptionDuringInitialization(e);
throw new RuntimeException(e);
}
}
- private void logGroovyException(Exception e) {
+ private void logScriptExceptionDuringInitialization(Exception e) {
try {
Rule rule = getRule();
if (rule instanceof ServiceProxy sp) {
diff --git a/core/src/main/java/com/predic8/membrane/core/lang/AbstractScriptInterceptor.java b/core/src/main/java/com/predic8/membrane/core/lang/AbstractScriptInterceptor.java
index 7116da27f3..23773e7ef2 100644
--- a/core/src/main/java/com/predic8/membrane/core/lang/AbstractScriptInterceptor.java
+++ b/core/src/main/java/com/predic8/membrane/core/lang/AbstractScriptInterceptor.java
@@ -33,6 +33,7 @@
import static com.predic8.membrane.core.interceptor.Interceptor.Flow.*;
import static com.predic8.membrane.core.interceptor.Outcome.*;
import static com.predic8.membrane.core.lang.ScriptingUtils.*;
+import static org.apache.commons.lang3.StringUtils.*;
public abstract class AbstractScriptInterceptor extends AbstractInterceptor {
@@ -73,12 +74,9 @@ protected Outcome runScript(Exchange exc, Flow flow) throws InterruptedException
Object res;
try {
res = script.apply(getParameterBindings(exc, flow, msg));
- } catch (Exception e) {
- log.warn(e.getMessage(), e);
- exc.setResponse(ProblemDetails.internal( router.isProduction())
- .title("Error executing script.")
- .detail("See logs for details.")
- .build());
+ }
+ catch (Exception e) {
+ handleScriptExecutionException(exc, e);
return RETURN;
}
@@ -114,7 +112,7 @@ protected Outcome runScript(Exchange exc, Flow flow) throws InterruptedException
return CONTINUE;
}
- // Test for packagename is needed cause the dependency is provided and maybe not on the classpath
+ // Test for package name is needed cause the dependency is provided and maybe not on the classpath
if(res.getClass().getPackageName().startsWith("org.graalvm.polyglot") && res instanceof Value value) {
Map m = value.as(Map.class);
msg.getHeader().setContentType(APPLICATION_JSON);
@@ -125,6 +123,26 @@ protected Outcome runScript(Exchange exc, Flow flow) throws InterruptedException
return CONTINUE;
}
+ protected void handleScriptExecutionException(Exchange exc, Exception e) {
+ log.warn("Error executing {} script: {}", name , e.getMessage());
+ log.warn("Script: {}", src);
+
+ ProblemDetails pd = ProblemDetails.internal(router.isProduction())
+ .title("Error executing script.");
+
+ if (!router.isProduction()) {
+ pd.extension("message", e.getMessage())
+ .extension("source", trim(src))
+ .extension("note", """
+ To hide error details set Membrane into production mode. In proxies.xml use ...");
+ """);
+ } else {
+ pd.detail("See logs for details.");
+ }
+
+ exc.setResponse(pd.build());
+ }
+
private HashMap getParameterBindings(Exchange exc, Flow flow, Message msg) {
HashMap parameterBindings = createParameterBindings(router.getUriFactory(), exc, msg, flow, scriptAccessesJson && msg.isJSON());
addOutcomeObjects(parameterBindings);
diff --git a/core/src/main/java/com/predic8/membrane/core/lang/ScriptingUtils.java b/core/src/main/java/com/predic8/membrane/core/lang/ScriptingUtils.java
index e973db3373..4c2c01947f 100644
--- a/core/src/main/java/com/predic8/membrane/core/lang/ScriptingUtils.java
+++ b/core/src/main/java/com/predic8/membrane/core/lang/ScriptingUtils.java
@@ -38,7 +38,11 @@ public class ScriptingUtils {
public static HashMap createParameterBindings(URIFactory uriFactory, Exchange exc, Message msg, Interceptor.Flow flow, boolean includeJsonObject) {
HashMap parameters = new HashMap<>();
+
+ // support both
parameters.put("exc", exc);
+ parameters.put("exchange", exc);
+
parameters.put("flow", flow);
if (flow == REQUEST) {