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) {